diff --git a/src/common/const.ts b/src/common/const.ts
index 76275f55a2..b4f79e5126 100644
--- a/src/common/const.ts
+++ b/src/common/const.ts
@@ -182,6 +182,7 @@ export const DOMAINS_WITH_CARD = [
"input_select",
"input_number",
"input_text",
+ "humidifier",
"lock",
"media_player",
"number",
diff --git a/src/common/entity/get_states.ts b/src/common/entity/get_states.ts
index 76e36ada3c..f8d10ad99f 100644
--- a/src/common/entity/get_states.ts
+++ b/src/common/entity/get_states.ts
@@ -135,6 +135,7 @@ const FIXED_DOMAIN_ATTRIBUTE_STATES = {
},
humidifier: {
device_class: ["humidifier", "dehumidifier"],
+ action: ["off", "idle", "humidifying", "drying"],
},
media_player: {
device_class: ["tv", "speaker", "receiver"],
diff --git a/src/components/ha-humidifier-state.ts b/src/components/ha-humidifier-state.ts
new file mode 100644
index 0000000000..fbf9cfe519
--- /dev/null
+++ b/src/components/ha-humidifier-state.ts
@@ -0,0 +1,137 @@
+import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
+import { customElement, property } from "lit/decorators";
+import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display";
+import { computeStateDisplay } from "../common/entity/compute_state_display";
+import { formatNumber } from "../common/number/format_number";
+import { blankBeforePercent } from "../common/translations/blank_before_percent";
+import { isUnavailableState, OFF } from "../data/entity";
+import { HumidifierEntity } from "../data/humidifier";
+import type { HomeAssistant } from "../types";
+
+@customElement("ha-humidifier-state")
+class HaHumidifierState extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj!: HumidifierEntity;
+
+ protected render(): TemplateResult {
+ const currentStatus = this._computeCurrentStatus();
+
+ return html`
+ ${!isUnavailableState(this.stateObj.state)
+ ? html`
+ ${this._localizeState()}
+ ${this.stateObj.attributes.mode
+ ? html`-
+ ${computeAttributeValueDisplay(
+ this.hass.localize,
+ this.stateObj,
+ this.hass.locale,
+ this.hass.config,
+ this.hass.entities,
+ "mode"
+ )}`
+ : ""}
+
+
${this._computeTarget()}
`
+ : this._localizeState()}
+
+
+ ${currentStatus && !isUnavailableState(this.stateObj.state)
+ ? html`
+ ${this.hass.localize("ui.card.climate.currently")}:
+
${currentStatus}
+
`
+ : ""}`;
+ }
+
+ private _computeCurrentStatus(): string | undefined {
+ if (!this.hass || !this.stateObj) {
+ return undefined;
+ }
+
+ if (this.stateObj.attributes.current_humidity != null) {
+ return `${formatNumber(
+ this.stateObj.attributes.current_humidity,
+ this.hass.locale
+ )}${blankBeforePercent(this.hass.locale)}%`;
+ }
+
+ return undefined;
+ }
+
+ private _computeTarget(): string {
+ if (!this.hass || !this.stateObj) {
+ return "";
+ }
+
+ if (this.stateObj.attributes.humidity != null) {
+ return `${formatNumber(
+ this.stateObj.attributes.humidity,
+ this.hass.locale
+ )}${blankBeforePercent(this.hass.locale)}%`;
+ }
+
+ return "";
+ }
+
+ private _localizeState(): string {
+ if (isUnavailableState(this.stateObj.state)) {
+ return this.hass.localize(`state.default.${this.stateObj.state}`);
+ }
+
+ const stateString = computeStateDisplay(
+ this.hass.localize,
+ this.stateObj,
+ this.hass.locale,
+ this.hass.config,
+ this.hass.entities
+ );
+
+ return this.stateObj.attributes.action && this.stateObj.state !== OFF
+ ? `${computeAttributeValueDisplay(
+ this.hass.localize,
+ this.stateObj,
+ this.hass.locale,
+ this.hass.config,
+ this.hass.entities,
+ "action"
+ )} (${stateString})`
+ : stateString;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ :host {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ white-space: nowrap;
+ }
+
+ .target {
+ color: var(--primary-text-color);
+ }
+
+ .current {
+ color: var(--secondary-text-color);
+ }
+
+ .state-label {
+ font-weight: bold;
+ text-transform: capitalize;
+ }
+
+ .unit {
+ display: inline-block;
+ direction: ltr;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-humidifier-state": HaHumidifierState;
+ }
+}
diff --git a/src/data/humidifier.ts b/src/data/humidifier.ts
index ace536b9ca..dd83e21003 100644
--- a/src/data/humidifier.ts
+++ b/src/data/humidifier.ts
@@ -2,21 +2,19 @@ import {
HassEntityAttributeBase,
HassEntityBase,
} from "home-assistant-js-websocket";
-import { FIXED_DOMAIN_STATES } from "../common/entity/get_states";
-import { UNAVAILABLE_STATES } from "./entity";
-type HumidifierState =
- | (typeof FIXED_DOMAIN_STATES.humidifier)[number]
- | (typeof UNAVAILABLE_STATES)[number];
+export type HumidifierState = "on" | "off";
+
+export type HumidifierAction = "off" | "idle" | "humidifying" | "drying";
export type HumidifierEntity = HassEntityBase & {
- state: HumidifierState;
attributes: HassEntityAttributeBase & {
humidity?: number;
current_humidity?: number;
min_humidity?: number;
max_humidity?: number;
mode?: string;
+ action: HumidifierAction;
available_modes?: string[];
};
};
diff --git a/src/dialogs/more-info/controls/more-info-humidifier.ts b/src/dialogs/more-info/controls/more-info-humidifier.ts
index 527c82461f..75dba4faf8 100644
--- a/src/dialogs/more-info/controls/more-info-humidifier.ts
+++ b/src/dialogs/more-info/controls/more-info-humidifier.ts
@@ -10,7 +10,10 @@ import {
import { property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../../common/dom/fire_event";
-import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
+import {
+ computeAttributeNameDisplay,
+ computeAttributeValueDisplay,
+} from "../../../common/entity/compute_attribute_display";
import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
@@ -22,6 +25,7 @@ import {
HUMIDIFIER_SUPPORT_MODES,
} from "../../../data/humidifier";
import { HomeAssistant } from "../../../types";
+import { computeStateDisplay } from "../../../common/entity/compute_state_display";
class MoreInfoHumidifier extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -49,7 +53,14 @@ class MoreInfoHumidifier extends LitElement {
})}
>
-
${hass.localize("ui.card.humidifier.humidity")}
+
+ ${computeAttributeNameDisplay(
+ hass.localize,
+ stateObj,
+ hass.entities,
+ "humidity"
+ )}
+
${stateObj.attributes.humidity} %
+
+
+ ${computeStateDisplay(
+ hass.localize,
+ stateObj,
+ hass.locale,
+ this.hass.config,
+ hass.entities,
+ "off"
+ )}
+
+
+ ${computeStateDisplay(
+ hass.localize,
+ stateObj,
+ hass.locale,
+ this.hass.config,
+ hass.entities,
+ "on"
+ )}
+
+
${supportModes
? html`
@@ -123,6 +163,16 @@ class MoreInfoHumidifier extends LitElement {
);
}
+ private _handleStateChanged(ev) {
+ const newVal = ev.target.value || null;
+ this._callServiceHelper(
+ this.stateObj!.state,
+ newVal,
+ newVal === "on" ? "turn_on" : "turn_off",
+ {}
+ );
+ }
+
private _handleModeChanged(ev) {
const newVal = ev.target.value || null;
this._callServiceHelper(
@@ -179,6 +229,11 @@ class MoreInfoHumidifier extends LitElement {
ha-select {
width: 100%;
+ margin-top: 8px;
+ }
+
+ ha-slider {
+ width: 100%;
}
.container-humidity .single-row {
diff --git a/src/panels/lovelace/cards/hui-tile-card.ts b/src/panels/lovelace/cards/hui-tile-card.ts
index 6afd8be31d..3471b6348b 100644
--- a/src/panels/lovelace/cards/hui-tile-card.ts
+++ b/src/panels/lovelace/cards/hui-tile-card.ts
@@ -225,15 +225,6 @@ 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,
@@ -251,6 +242,16 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
return `${stateDisplay} ⸱ ${positionStateDisplay}`;
}
}
+
+ if (domain === "humidifier" && stateActive(stateObj)) {
+ const humidity = (stateObj as HumidifierEntity).attributes.humidity;
+ if (humidity) {
+ return `${stateDisplay} ⸱ ${Math.round(humidity)}${blankBeforePercent(
+ this.hass!.locale
+ )}%`;
+ }
+ }
+
return stateDisplay;
}
diff --git a/src/panels/lovelace/cards/tile/badges/tile-badge-humidifier.ts b/src/panels/lovelace/cards/tile/badges/tile-badge-humidifier.ts
new file mode 100644
index 0000000000..c4901e0b2d
--- /dev/null
+++ b/src/panels/lovelace/cards/tile/badges/tile-badge-humidifier.ts
@@ -0,0 +1,41 @@
+import {
+ mdiArrowDownBold,
+ mdiArrowUpBold,
+ mdiClockOutline,
+ mdiPower,
+} from "@mdi/js";
+import { stateColorCss } from "../../../../../common/entity/state_color";
+import {
+ HumidifierAction,
+ HumidifierEntity,
+ HumidifierState,
+} from "../../../../../data/humidifier";
+import { ComputeBadgeFunction } from "./tile-badge";
+
+export const HUMIDIFIER_ACTION_ICONS: Record = {
+ drying: mdiArrowDownBold,
+ humidifying: mdiArrowUpBold,
+ idle: mdiClockOutline,
+ off: mdiPower,
+};
+
+export const HUMIDIFIER_ACTION_MODE: Record =
+ {
+ drying: "on",
+ humidifying: "on",
+ idle: "off",
+ off: "off",
+ };
+
+export const computeHumidifierBadge: ComputeBadgeFunction = (stateObj) => {
+ const hvacAction = (stateObj as HumidifierEntity).attributes.action;
+
+ if (!hvacAction || hvacAction === "off") {
+ return undefined;
+ }
+
+ return {
+ iconPath: HUMIDIFIER_ACTION_ICONS[hvacAction],
+ color: stateColorCss(stateObj, HUMIDIFIER_ACTION_MODE[hvacAction]),
+ };
+};
diff --git a/src/panels/lovelace/cards/tile/badges/tile-badge.ts b/src/panels/lovelace/cards/tile/badges/tile-badge.ts
index e303c49129..5281f3591d 100644
--- a/src/panels/lovelace/cards/tile/badges/tile-badge.ts
+++ b/src/panels/lovelace/cards/tile/badges/tile-badge.ts
@@ -5,6 +5,7 @@ import { UNAVAILABLE, UNKNOWN } from "../../../../../data/entity";
import { HomeAssistant } from "../../../../../types";
import { computeClimateBadge } from "./tile-badge-climate";
import { computePersonBadge } from "./tile-badge-person";
+import { computeHumidifierBadge } from "./tile-badge-humidifier";
export type TileBadge = {
color?: string;
@@ -34,6 +35,8 @@ export const computeTileBadge: ComputeBadgeFunction = (stateObj, hass) => {
return computePersonBadge(stateObj, hass);
case "climate":
return computeClimateBadge(stateObj, hass);
+ case "humidifier":
+ return computeHumidifierBadge(stateObj, hass);
default:
return undefined;
}
diff --git a/src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts b/src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts
index 4aa7738253..4463710945 100644
--- a/src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts
+++ b/src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts
@@ -1,11 +1,18 @@
-import { html, LitElement, PropertyValues, nothing } from "lit";
+import {
+ CSSResultGroup,
+ LitElement,
+ PropertyValues,
+ css,
+ html,
+ nothing,
+} from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/entity/ha-entity-toggle";
+import "../../../components/ha-humidifier-state";
import { HumidifierEntity } from "../../../data/humidifier";
import { HomeAssistant } from "../../../types";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
-import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import { EntityConfig, LovelaceRow } from "./types";
@@ -43,32 +50,20 @@ class HuiHumidifierEntityRow extends LitElement implements LovelaceRow {
}
return html`
-
-
+
+
+
`;
}
+
+ static get styles(): CSSResultGroup {
+ return css`
+ ha-humidifier-state {
+ text-align: right;
+ }
+ `;
+ }
}
declare global {
diff --git a/src/state-summary/state-card-content.js b/src/state-summary/state-card-content.js
index 21ca9d08c1..b1669ae994 100644
--- a/src/state-summary/state-card-content.js
+++ b/src/state-summary/state-card-content.js
@@ -4,6 +4,7 @@ import dynamicContentUpdater from "../common/dom/dynamic_content_updater";
import { stateCardType } from "../common/entity/state_card_type";
import "./state-card-button";
import "./state-card-climate";
+import "./state-card-humidifier";
import "./state-card-configurator";
import "./state-card-cover";
import "./state-card-display";
diff --git a/src/state-summary/state-card-humidifier.js b/src/state-summary/state-card-humidifier.js
new file mode 100644
index 0000000000..a99b6a539d
--- /dev/null
+++ b/src/state-summary/state-card-humidifier.js
@@ -0,0 +1,55 @@
+import "@polymer/iron-flex-layout/iron-flex-layout-classes";
+import { html } from "@polymer/polymer/lib/utils/html-tag";
+/* eslint-plugin-disable lit */
+import { PolymerElement } from "@polymer/polymer/polymer-element";
+import "../components/entity/state-info";
+import "../components/ha-humidifier-state";
+
+class StateCardHumidifier extends PolymerElement {
+ static get template() {
+ return html`
+
+
+
+
+ ${this.stateInfoTemplate}
+
+
+ `;
+ }
+
+ static get stateInfoTemplate() {
+ return html`
+
+ `;
+ }
+
+ static get properties() {
+ return {
+ hass: Object,
+ stateObj: Object,
+ inDialog: {
+ type: Boolean,
+ value: false,
+ },
+ };
+ }
+}
+customElements.define("state-card-humidifier", StateCardHumidifier);
diff --git a/src/translations/en.json b/src/translations/en.json
index 371c4c706f..68dd5c721f 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -131,6 +131,7 @@
},
"humidifier": {
"humidity": "Target humidity",
+ "state": "State",
"mode": "Mode",
"target_humidity_entity": "{name} target humidity",
"on_entity": "{name} on"