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"