diff --git a/setup.py b/setup.py index f369f06592..bdc1cc932a 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name="home-assistant-frontend", - version="20200622.0", + version="20200623.0", description="The Home Assistant frontend", url="https://github.com/home-assistant/home-assistant-polymer", author="The Home Assistant Authors", diff --git a/src/common/const.ts b/src/common/const.ts index 503e866c4e..b801ae37fc 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -37,6 +37,7 @@ export const DOMAINS_WITH_MORE_INFO = [ "fan", "group", "history_graph", + "humidifier", "input_datetime", "light", "lock", @@ -79,6 +80,7 @@ export const DOMAINS_TOGGLE = new Set([ "switch", "group", "automation", + "humidifier", ]); /** Temperature units. */ diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 82eb783dfa..10de4cb847 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -55,6 +55,12 @@ export const computeStateDisplay = ( return formatDateTime(date, language); } + if (domain === "humidifier") { + if (stateObj.state === "on" && stateObj.attributes.humidity) { + return `${stateObj.attributes.humidity}%`; + } + } + return ( // Return device class translation (stateObj.attributes.device_class && diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts index cf0617923b..632473bafd 100644 --- a/src/common/entity/domain_icon.ts +++ b/src/common/entity/domain_icon.ts @@ -22,6 +22,7 @@ const fixedIcons = { history_graph: "hass:chart-line", homeassistant: "hass:home-assistant", homekit: "hass:home-automation", + humidifier: "hass:air-humidifier", image_processing: "hass:image-filter-frames", input_boolean: "hass:toggle-switch-outline", input_datetime: "hass:calendar-clock", diff --git a/src/common/style/icon_color_css.ts b/src/common/style/icon_color_css.ts index 32adb53377..b2bb4e5d95 100644 --- a/src/common/style/icon_color_css.ts +++ b/src/common/style/icon_color_css.ts @@ -8,6 +8,7 @@ export const iconColorCSS = css` ha-icon[data-domain="camera"][data-state="streaming"], ha-icon[data-domain="cover"][data-state="open"], ha-icon[data-domain="fan"][data-state="on"], + ha-icon[data-domain="humidifier"][data-state="on"], ha-icon[data-domain="light"][data-state="on"], ha-icon[data-domain="input_boolean"][data-state="on"], ha-icon[data-domain="lock"][data-state="unlocked"], diff --git a/src/components/entity/ha-entity-toggle.ts b/src/components/entity/ha-entity-toggle.ts index 7b19129c7e..587f299641 100644 --- a/src/components/entity/ha-entity-toggle.ts +++ b/src/components/entity/ha-entity-toggle.ts @@ -22,7 +22,7 @@ const isOn = (stateObj?: HassEntity) => !STATES_OFF.includes(stateObj.state) && !UNAVAILABLE_STATES.includes(stateObj.state); -class HaEntityToggle extends LitElement { +export class HaEntityToggle extends LitElement { // hass is not a property so that we only re-render on stateObj changes public hass?: HomeAssistant; diff --git a/src/components/state-history-chart-line.js b/src/components/state-history-chart-line.js index 4cc1321475..3a857b8663 100644 --- a/src/components/state-history-chart-line.js +++ b/src/components/state-history-chart-line.js @@ -262,6 +262,28 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) { pushData(new Date(state.last_changed), series); } }); + } else if (domain === "humidifier") { + addColumn( + `${this.hass.localize( + "ui.card.humidifier.target_humidity_entity", + "name", + name + )}`, + true + ); + addColumn( + `${this.hass.localize("ui.card.humidifier.on_entity", "name", name)}`, + true, + true + ); + + states.states.forEach((state) => { + if (!state.attributes) return; + const target = safeParseFloat(state.attributes.humidity); + const series = [target]; + series.push(state.state === "on" ? target : null); + pushData(new Date(state.last_changed), series); + }); } else { // Only disable interpolation for sensors const isStep = domain === "sensor"; diff --git a/src/data/history.ts b/src/data/history.ts index 39c383659d..c5df270404 100644 --- a/src/data/history.ts +++ b/src/data/history.ts @@ -5,13 +5,15 @@ import { computeStateName } from "../common/entity/compute_state_name"; import { LocalizeFunc } from "../common/translations/localize"; import { HomeAssistant } from "../types"; -const DOMAINS_USE_LAST_UPDATED = ["climate", "water_heater"]; +const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"]; const LINE_ATTRIBUTES_TO_KEEP = [ "temperature", "current_temperature", "target_temp_low", "target_temp_high", "hvac_action", + "humidity", + "mode", ]; export interface LineChartState { @@ -224,6 +226,8 @@ export const computeHistory = ( unit = hass.config.unit_system.temperature; } else if (computeStateDomain(stateInfo[0]) === "water_heater") { unit = hass.config.unit_system.temperature; + } else if (computeStateDomain(stateInfo[0]) === "humidifier") { + unit = "%"; } if (!unit) { diff --git a/src/data/humidifier.ts b/src/data/humidifier.ts new file mode 100644 index 0000000000..968aad1dcd --- /dev/null +++ b/src/data/humidifier.ts @@ -0,0 +1,19 @@ +import { + HassEntityAttributeBase, + HassEntityBase, +} from "home-assistant-js-websocket"; + +export type HumidifierEntity = HassEntityBase & { + attributes: HassEntityAttributeBase & { + humidity?: number; + min_humidity?: number; + max_humidity?: number; + mode?: string; + available_modes?: string[]; + }; +}; + +export const HUMIDIFIER_SUPPORT_MODES = 1; + +export const HUMIDIFIER_DEVICE_CLASS_HUMIDIFIER = "humidifier"; +export const HUMIDIFIER_DEVICE_CLASS_DEHUMIDIFIER = "dehumidifier"; diff --git a/src/dialogs/more-info/controls/more-info-content.ts b/src/dialogs/more-info/controls/more-info-content.ts index 896d1c576b..2f7ed3dbf7 100644 --- a/src/dialogs/more-info/controls/more-info-content.ts +++ b/src/dialogs/more-info/controls/more-info-content.ts @@ -14,6 +14,7 @@ import "./more-info-default"; import "./more-info-fan"; import "./more-info-group"; import "./more-info-history_graph"; +import "./more-info-humidifier"; import "./more-info-input_datetime"; import "./more-info-light"; import "./more-info-lock"; diff --git a/src/dialogs/more-info/controls/more-info-humidifier.ts b/src/dialogs/more-info/controls/more-info-humidifier.ts new file mode 100644 index 0000000000..6a3bbc1aad --- /dev/null +++ b/src/dialogs/more-info/controls/more-info-humidifier.ts @@ -0,0 +1,218 @@ +import "@polymer/iron-flex-layout/iron-flex-layout-classes"; +import "@polymer/paper-item/paper-item"; +import "@polymer/paper-listbox/paper-listbox"; +import { + css, + CSSResult, + html, + LitElement, + property, + PropertyValues, + TemplateResult, +} from "lit-element"; +import { classMap } from "lit-html/directives/class-map"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { supportsFeature } from "../../../common/entity/supports-feature"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; +import "../../../components/ha-paper-dropdown-menu"; +import "../../../components/ha-paper-slider"; +import "../../../components/ha-switch"; +import { + HumidifierEntity, + HUMIDIFIER_SUPPORT_MODES, +} from "../../../data/humidifier"; +import { HomeAssistant } from "../../../types"; + +class MoreInfoHumidifier extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public stateObj?: HumidifierEntity; + + private _resizeDebounce?: number; + + protected render(): TemplateResult { + if (!this.stateObj) { + return html``; + } + + const hass = this.hass; + const stateObj = this.stateObj; + + const supportModes = supportsFeature(stateObj, HUMIDIFIER_SUPPORT_MODES); + + const rtlDirection = computeRTLDirection(hass); + + return html` +
+
+
${hass.localize("ui.card.humidifier.humidity")}
+
+
+ ${stateObj.attributes.humidity} % +
+ + +
+
+ + ${supportModes + ? html` +
+ + + ${stateObj.attributes.available_modes!.map( + (mode) => html` + + ${hass.localize( + `state_attributes.humidifier.mode.${mode}` + ) || mode} + + ` + )} + + +
+ ` + : ""} +
+ `; + } + + protected updated(changedProps: PropertyValues) { + super.updated(changedProps); + if (!changedProps.has("stateObj") || !this.stateObj) { + return; + } + + if (this._resizeDebounce) { + clearTimeout(this._resizeDebounce); + } + this._resizeDebounce = window.setTimeout(() => { + fireEvent(this, "iron-resize"); + this._resizeDebounce = undefined; + }, 500); + } + + private _targetHumiditySliderChanged(ev) { + const newVal = ev.target.value; + this._callServiceHelper( + this.stateObj!.attributes.humidity, + newVal, + "set_humidity", + { humidity: newVal } + ); + } + + private _handleModeChanged(ev) { + const newVal = ev.detail.value || null; + this._callServiceHelper( + this.stateObj!.attributes.mode, + newVal, + "set_mode", + { mode: newVal } + ); + } + + private async _callServiceHelper( + oldVal: unknown, + newVal: unknown, + service: string, + data: { + entity_id?: string; + [key: string]: unknown; + } + ) { + if (oldVal === newVal) { + return; + } + + data.entity_id = this.stateObj!.entity_id; + const curState = this.stateObj; + + await this.hass.callService("humidifier", service, data); + + // We reset stateObj to re-sync the inputs with the state. It will be out + // of sync if our service call did not result in the entity to be turned + // on. Since the state is not changing, the resync is not called automatic. + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // No need to resync if we received a new state. + if (this.stateObj !== curState) { + return; + } + + this.stateObj = undefined; + await this.updateComplete; + // Only restore if not set yet by a state change + if (this.stateObj === undefined) { + this.stateObj = curState; + } + } + + static get styles(): CSSResult { + return css` + :host { + color: var(--primary-text-color); + } + + ha-paper-dropdown-menu { + width: 100%; + } + + paper-item { + cursor: pointer; + } + + ha-paper-slider { + width: 100%; + } + + .container-humidity .single-row { + display: flex; + height: 50px; + } + + .target-humidity { + width: 90px; + font-size: 200%; + margin: auto; + direction: ltr; + } + + .humidity { + --paper-slider-active-color: var(--paper-blue-400); + --paper-slider-secondary-color: var(--paper-blue-400); + } + + .single-row { + padding: 8px 0; + } + `; + } +} + +customElements.define("more-info-humidifier", MoreInfoHumidifier); diff --git a/src/layouts/home-assistant.ts b/src/layouts/home-assistant.ts index 61a1d365a8..69e6bd677e 100644 --- a/src/layouts/home-assistant.ts +++ b/src/layouts/home-assistant.ts @@ -22,6 +22,10 @@ export class HomeAssistantAppEl extends HassElement { private _haVersion?: string; + private _hiddenTimeout?: number; + + private _visiblePromiseResolve?: () => void; + protected render() { const hass = this.hass; @@ -71,6 +75,12 @@ export class HomeAssistantAppEl extends HassElement { super.hassConnected(); // @ts-ignore this._loadHassTranslations(this.hass!.language, "state"); + + document.addEventListener( + "visibilitychange", + () => this.__handleVisibilityChange(), + false + ); } protected hassReconnected() { @@ -137,6 +147,33 @@ export class HomeAssistantAppEl extends HassElement { ? route.path.substr(1) : route.path.substr(1, dividerPos - 1); } + + private __handleVisibilityChange() { + if (document.hidden) { + // If the document is hidden, we will prevent reconnects until we are visible again + this.hass!.connection.suspendReconnectUntil( + new Promise((resolve) => { + this._visiblePromiseResolve = resolve; + }) + ); + // We close the connection to Home Assistant after being hidden for 5 minutes + this._hiddenTimeout = window.setTimeout(() => { + this._hiddenTimeout = undefined; + this.hass!.connection.suspend(); + }, 300000); + } else { + // Clear timer to close the connection + if (this._hiddenTimeout) { + clearTimeout(this._hiddenTimeout); + this._hiddenTimeout = undefined; + } + // Unsuspend the reconnect + if (this._visiblePromiseResolve) { + this._visiblePromiseResolve(); + this._visiblePromiseResolve = undefined; + } + } + } } declare global { diff --git a/src/panels/lovelace/cards/hui-humidifier-card.ts b/src/panels/lovelace/cards/hui-humidifier-card.ts new file mode 100644 index 0000000000..dd2bd4f7b0 --- /dev/null +++ b/src/panels/lovelace/cards/hui-humidifier-card.ts @@ -0,0 +1,392 @@ +import "../../../components/ha-icon-button"; +import "@thomasloven/round-slider"; +import { HassEntity } from "home-assistant-js-websocket"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + PropertyValues, + svg, + TemplateResult, +} from "lit-element"; +import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; +import { fireEvent } from "../../../common/dom/fire_event"; +import { computeStateName } from "../../../common/entity/compute_state_name"; +import { computeRTLDirection } from "../../../common/util/compute_rtl"; +import "../../../components/ha-card"; +import { HumidifierEntity } from "../../../data/humidifier"; +import { UNAVAILABLE_STATES } from "../../../data/entity"; +import { HomeAssistant } from "../../../types"; +import { findEntities } from "../common/find-entites"; +import { hasConfigOrEntityChanged } from "../common/has-changed"; +import { createEntityNotFoundWarning } from "../components/hui-warning"; +import { LovelaceCard, LovelaceCardEditor } from "../types"; +import { HumidifierCardConfig } from "./types"; + +@customElement("hui-humidifier-card") +export class HuiHumidifierCard extends LitElement implements LovelaceCard { + public static async getConfigElement(): Promise { + await import( + /* webpackChunkName: "hui-humidifier-card-editor" */ "../editor/config-elements/hui-humidifier-card-editor" + ); + return document.createElement("hui-humidifier-card-editor"); + } + + public static getStubConfig( + hass: HomeAssistant, + entities: string[], + entitiesFallback: string[] + ): HumidifierCardConfig { + const includeDomains = ["humidifier"]; + const maxEntities = 1; + const foundEntities = findEntities( + hass, + maxEntities, + entities, + entitiesFallback, + includeDomains + ); + + return { type: "humidifier", entity: foundEntities[0] || "" }; + } + + @property() public hass?: HomeAssistant; + + @property() private _config?: HumidifierCardConfig; + + @property() private _setHum?: number; + + public getCardSize(): number { + return 5; + } + + public setConfig(config: HumidifierCardConfig): void { + if (!config.entity || config.entity.split(".")[0] !== "humidifier") { + throw new Error("Specify an entity from within the humidifier domain."); + } + + this._config = config; + } + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + const stateObj = this.hass.states[this._config.entity] as HumidifierEntity; + + if (!stateObj) { + return html` + + ${createEntityNotFoundWarning(this.hass, this._config.entity)} + + `; + } + + const name = + this._config!.name || + computeStateName(this.hass!.states[this._config!.entity]); + const targetHumidity = + stateObj.attributes.humidity !== null && + Number.isFinite(Number(stateObj.attributes.humidity)) + ? stateObj.attributes.humidity + : stateObj.attributes.min_humidity; + + const rtlDirection = computeRTLDirection(this.hass); + + const slider = UNAVAILABLE_STATES.includes(stateObj.state) + ? html` ` + : html` + + `; + + const setValues = svg` + + + ${ + UNAVAILABLE_STATES.includes(stateObj.state) || + this._setHum === undefined || + this._setHum === null + ? "" + : svg` + ${this._setHum.toFixed()} + + % + + ` + } + + + + + + ${this.hass!.localize(`state.default.${stateObj.state}`)} + ${ + stateObj.attributes.mode && + !UNAVAILABLE_STATES.includes(stateObj.state) + ? html` + - + ${this.hass!.localize( + `state_attributes.humidifier.mode.${stateObj.attributes.mode}` + ) || stateObj.attributes.mode} + ` + : "" + } + + + + `; + + return html` + + + +
+
+
+ ${slider} +
+
+ ${setValues} +
+
+
+
+
+ ${name} +
+
+
+ `; + } + + protected shouldUpdate(changedProps: PropertyValues): boolean { + return hasConfigOrEntityChanged(this, changedProps); + } + + protected updated(changedProps: PropertyValues): void { + super.updated(changedProps); + + if ( + !this._config || + !this.hass || + (!changedProps.has("hass") && !changedProps.has("_config")) + ) { + return; + } + + const oldHass = changedProps.get("hass") as HomeAssistant | undefined; + const oldConfig = changedProps.get("_config") as + | HumidifierCardConfig + | undefined; + + if ( + !oldHass || + !oldConfig || + oldHass.themes !== this.hass.themes || + oldConfig.theme !== this._config.theme + ) { + applyThemesOnElement(this, this.hass.themes, this._config.theme); + } + + const stateObj = this.hass.states[this._config.entity]; + if (!stateObj) { + return; + } + + if (!oldHass || oldHass.states[this._config.entity] !== stateObj) { + this._setHum = this._getSetHum(stateObj); + this._rescale_svg(); + } + } + + private _rescale_svg() { + // Set the viewbox of the SVG containing the set humidity to perfectly + // fit the text + // That way it will auto-scale correctly + // This is not done to the SVG containing the current humidity, because + // it should not be centered on the text, but only on the value + if (this.shadowRoot && this.shadowRoot.querySelector("ha-card")) { + (this.shadowRoot.querySelector( + "ha-card" + ) as LitElement).updateComplete.then(() => { + const svgRoot = this.shadowRoot!.querySelector("#set-values"); + const box = svgRoot!.querySelector("g")!.getBBox(); + svgRoot!.setAttribute( + "viewBox", + `${box!.x} ${box!.y} ${box!.width} ${box!.height}` + ); + svgRoot!.setAttribute("width", `${box!.width}`); + svgRoot!.setAttribute("height", `${box!.height}`); + }); + } + } + + private _getSetHum(stateObj: HassEntity): undefined | number { + if (UNAVAILABLE_STATES.includes(stateObj.state)) { + return undefined; + } + + return stateObj.attributes.humidity; + } + + private _dragEvent(e): void { + this._setHum = e.detail.value; + } + + private _setHumidity(e): void { + this.hass!.callService("humidifier", "set_humidity", { + entity_id: this._config!.entity, + humidity: e.detail.value, + }); + } + + private _handleMoreInfo() { + fireEvent(this, "hass-more-info", { + entityId: this._config!.entity, + }); + } + + static get styles(): CSSResult { + return css` + :host { + display: block; + } + + ha-card { + height: 100%; + position: relative; + overflow: hidden; + --name-font-size: 1.2rem; + --brightness-font-size: 1.2rem; + --rail-border-color: transparent; + } + + .more-info { + position: absolute; + cursor: pointer; + top: 0; + right: 0; + border-radius: 100%; + color: var(--secondary-text-color); + z-index: 25; + } + + .content { + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + } + + #controls { + display: flex; + justify-content: center; + padding: 16px; + position: relative; + } + + #slider { + height: 100%; + width: 100%; + position: relative; + max-width: 250px; + min-width: 100px; + } + + round-slider { + --round-slider-path-color: var(--disabled-text-color); + --round-slider-bar-color: var(--mode-color); + padding-bottom: 10%; + } + + #slider-center { + position: absolute; + width: calc(100% - 40px); + height: calc(100% - 40px); + box-sizing: border-box; + border-radius: 100%; + left: 20px; + top: 20px; + text-align: center; + overflow-wrap: break-word; + pointer-events: none; + } + + #humidity { + position: absolute; + transform: translate(-50%, -50%); + width: 100%; + height: 50%; + top: 45%; + left: 50%; + } + + #set-values { + max-width: 80%; + transform: translate(0, -50%); + font-size: 20px; + } + + #set-mode { + fill: var(--secondary-text-color); + font-size: 16px; + } + + #info { + display: flex-vertical; + justify-content: center; + text-align: center; + padding: 16px; + margin-top: -60px; + font-size: var(--name-font-size); + } + + #modes > * { + color: var(--disabled-text-color); + cursor: pointer; + display: inline-block; + } + + #modes .selected-icon { + color: var(--mode-color); + } + + text { + fill: var(--primary-text-color); + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-humidifier-card": HuiHumidifierCard; + } +} diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 3ce3342309..c003fb32ab 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -133,6 +133,12 @@ export interface GlanceCardConfig extends LovelaceCardConfig { state_color?: boolean; } +export interface HumidifierCardConfig extends LovelaceCardConfig { + entity: string; + theme?: string; + name?: string; +} + export interface IframeCardConfig extends LovelaceCardConfig { aspect_ratio?: string; title?: string; diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 8941ed45d2..9b3f1c830b 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -37,6 +37,7 @@ import { GroupEntity, HomeAssistant } from "../../../types"; import { AlarmPanelCardConfig, EntitiesCardConfig, + HumidifierCardConfig, LightCardConfig, PictureEntityCardConfig, ThermostatCardConfig, @@ -150,6 +151,12 @@ export const computeCards = ( refresh_interval: stateObj.attributes.refresh, }; cards.push(cardConfig); + } else if (domain === "humidifier") { + const cardConfig: HumidifierCardConfig = { + type: "humidifier", + entity: entityId, + }; + cards.push(cardConfig); } else if (domain === "light" && single) { const cardConfig: LightCardConfig = { type: "light", diff --git a/src/panels/lovelace/create-element/create-card-element.ts b/src/panels/lovelace/create-element/create-card-element.ts index 23b975e0ff..e7061c1727 100644 --- a/src/panels/lovelace/create-element/create-card-element.ts +++ b/src/panels/lovelace/create-element/create-card-element.ts @@ -38,6 +38,7 @@ const LAZY_LOAD_TYPES = { "empty-state": () => import("../cards/hui-empty-state-card"), starting: () => import("../cards/hui-starting-card"), "entity-filter": () => import("../cards/hui-entity-filter-card"), + humidifier: () => import("../cards/hui-humidifier-card"), "media-control": () => import("../cards/hui-media-control-card"), "picture-elements": () => import("../cards/hui-picture-elements-card"), "picture-entity": () => import("../cards/hui-picture-entity-card"), diff --git a/src/panels/lovelace/create-element/create-row-element.ts b/src/panels/lovelace/create-element/create-row-element.ts index 80533bf0e5..178192fa48 100644 --- a/src/panels/lovelace/create-element/create-row-element.ts +++ b/src/panels/lovelace/create-element/create-row-element.ts @@ -51,6 +51,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { cover: "cover", fan: "toggle", group: "group", + humidifier: "toggle", input_boolean: "toggle", input_number: "input-number", input_select: "input-select", diff --git a/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts new file mode 100644 index 0000000000..b293953407 --- /dev/null +++ b/src/panels/lovelace/editor/config-elements/hui-humidifier-card-editor.ts @@ -0,0 +1,117 @@ +import "@polymer/paper-input/paper-input"; +import { + customElement, + html, + LitElement, + property, + TemplateResult, +} from "lit-element"; +import { fireEvent } from "../../../../common/dom/fire_event"; +import "../../../../components/entity/ha-entity-picker"; +import { HomeAssistant } from "../../../../types"; +import { HumidifierCardConfig } from "../../cards/types"; +import { struct } from "../../common/structs/struct"; +import "../../components/hui-theme-select-editor"; +import { LovelaceCardEditor } from "../../types"; +import { EditorTarget, EntitiesEditorEvent } from "../types"; +import { configElementStyle } from "./config-elements-style"; + +const cardConfigStruct = struct({ + type: "string", + entity: "string", + name: "string?", + theme: "string?", +}); + +const includeDomains = ["humidifier"]; + +@customElement("hui-humidifier-card-editor") +export class HuiHumidifierCardEditor extends LitElement + implements LovelaceCardEditor { + @property() public hass?: HomeAssistant; + + @property() private _config?: HumidifierCardConfig; + + public setConfig(config: HumidifierCardConfig): void { + config = cardConfigStruct(config); + this._config = config; + } + + get _entity(): string { + return this._config!.entity || ""; + } + + get _name(): string { + return this._config!.name || ""; + } + + get _theme(): string { + return this._config!.theme || ""; + } + + protected render(): TemplateResult { + if (!this.hass || !this._config) { + return html``; + } + + return html` + ${configElementStyle} +
+ + + +
+ `; + } + + private _valueChanged(ev: EntitiesEditorEvent): void { + if (!this._config || !this.hass) { + return; + } + const target = ev.target! as EditorTarget; + + if (this[`_${target.configValue}`] === target.value) { + return; + } + if (target.configValue) { + if (target.value === "") { + delete this._config[target.configValue!]; + } else { + this._config = { ...this._config, [target.configValue!]: target.value }; + } + } + fireEvent(this, "config-changed", { config: this._config }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-humidifier-card-editor": HuiHumidifierCardEditor; + } +} diff --git a/src/panels/lovelace/editor/lovelace-cards.ts b/src/panels/lovelace/editor/lovelace-cards.ts index 210f3f834d..9d777fa942 100644 --- a/src/panels/lovelace/editor/lovelace-cards.ts +++ b/src/panels/lovelace/editor/lovelace-cards.ts @@ -29,6 +29,10 @@ export const coreCards: Card[] = [ type: "history-graph", showElement: true, }, + { + type: "humidifier", + showElement: true, + }, { type: "light", showElement: true, diff --git a/src/state/hass-element.ts b/src/state/hass-element.ts index 3b702e7bb1..a5cb0992ae 100644 --- a/src/state/hass-element.ts +++ b/src/state/hass-element.ts @@ -12,7 +12,6 @@ import SidebarMixin from "./sidebar-mixin"; import ThemesMixin from "./themes-mixin"; import TranslationsMixin from "./translations-mixin"; import { urlSyncMixin } from "./url-sync-mixin"; -import { suspendMixin } from "./suspend-mixin"; const ext = (baseClass: T, mixins): T => mixins.reduceRight((base, mixin) => mixin(base), baseClass); @@ -25,7 +24,6 @@ export class HassElement extends ext(HassBaseEl, [ SidebarMixin, DisconnectToastMixin, connectionMixin, - suspendMixin, NotificationMixin, dialogManagerMixin, urlSyncMixin, diff --git a/src/state/suspend-mixin.ts b/src/state/suspend-mixin.ts deleted file mode 100644 index 774a77a45f..0000000000 --- a/src/state/suspend-mixin.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Constructor } from "../types"; -import { HassBaseEl } from "./hass-base-mixin"; - -export const suspendMixin = >( - superClass: T -) => - class extends superClass { - private __hiddenTimeout?: number; - - private __visiblePromiseResolve?: () => void; - - protected hassConnected() { - super.hassConnected(); - - document.addEventListener( - "visibilitychange", - () => this.__handleVisibilityChange(), - false - ); - } - - private __handleVisibilityChange() { - if (document.hidden) { - // If the document is hidden, we will prevent reconnects until we are visible again - this.hass!.connection.suspendReconnectUntil( - new Promise((resolve) => { - this.__visiblePromiseResolve = resolve; - }) - ); - // We close the connection to Home Assistant after being hidden for 5 minutes - this.__hiddenTimeout = window.setTimeout(() => { - this.__hiddenTimeout = undefined; - this.hass!.connection.suspend(); - }, 300000); - } else { - // Clear timer to close the connection - if (this.__hiddenTimeout) { - clearTimeout(this.__hiddenTimeout); - this.__hiddenTimeout = undefined; - } - // Unsuspend the reconnect - if (this.__visiblePromiseResolve) { - this.__visiblePromiseResolve(); - this.__visiblePromiseResolve = undefined; - } - } - } - }; diff --git a/src/translations/en.json b/src/translations/en.json index 6c19dc6969..3045293b40 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -44,6 +44,19 @@ "idle": "Idle", "fan": "Fan" } + }, + "humidifier": { + "mode": { + "normal": "Normal", + "eco": "Eco", + "away": "Away", + "boost": "Boost", + "comfort": "Comfort", + "home": "Home", + "sleep": "Sleep", + "auto": "Auto", + "baby": "Baby" + } } }, "state_badge": { @@ -146,6 +159,12 @@ "forward": "Forward", "reverse": "Reverse" }, + "humidifier": { + "humidity": "Target humidity", + "mode": "Mode", + "target_humidity_entity": "{name} target humidity", + "on_entity": "{name} on" + }, "light": { "brightness": "Brightness", "color_temperature": "Color temperature", @@ -1935,6 +1954,10 @@ "name": "Horizontal Stack", "description": "The Horizontal Stack card allows you to stack together multiple cards, so they always sit next to each other in the space of one column." }, + "humidifier": { + "name": "Humidifier", + "description": "The Humidifier card gives control of your humidifier entity. Allowing you to change the humidity and mode of the entity." + }, "iframe": { "name": "Webpage", "description": "The Webpage card allows you to embed your favorite webpage right into Home Assistant." diff --git a/src/util/hass-attributes-util.js b/src/util/hass-attributes-util.js index 05fa3ebb63..4eb17735a0 100644 --- a/src/util/hass-attributes-util.js +++ b/src/util/hass-attributes-util.js @@ -37,6 +37,7 @@ hassAttributeUtil.DOMAIN_DEVICE_CLASS = { "shutter", "window", ], + humidifier: ["dehumidifier", "humidifier"], sensor: [ "battery", "humidity", @@ -89,7 +90,7 @@ hassAttributeUtil.LOGIC_STATE_ATTRIBUTES = hassAttributeUtil.LOGIC_STATE_ATTRIBU type: "array", options: hassAttributeUtil.DOMAIN_DEVICE_CLASS, description: "Device class", - domains: ["binary_sensor", "cover", "sensor", "switch"], + domains: ["binary_sensor", "cover", "humidifier", "sensor", "switch"], }, hidden: { type: "boolean", description: "Hide from UI" }, assumed_state: { @@ -100,6 +101,7 @@ hassAttributeUtil.LOGIC_STATE_ATTRIBUTES = hassAttributeUtil.LOGIC_STATE_ATTRIBU "cover", "climate", "fan", + "humidifier", "group", "water_heater", ], diff --git a/translations/frontend/da.json b/translations/frontend/da.json index 2969ff8680..bcef048d21 100644 --- a/translations/frontend/da.json +++ b/translations/frontend/da.json @@ -506,6 +506,11 @@ "clear": "Ryd", "show_areas": "Vis områder" }, + "date-range-picker": { + "end_date": "Slutdato", + "select": "Vælg", + "start_date": "Startdato" + }, "device-picker": { "clear": "Ryd", "device": "Enhed", @@ -695,6 +700,7 @@ "zha_device_info": { "buttons": { "add": "Tilføj enheder", + "clusters": "Administrer klynger", "reconfigure": "Genkonfigurer enhed", "remove": "Fjern enhed", "zigbee_information": "Zigbee-oplysninger" @@ -1558,6 +1564,7 @@ } }, "mqtt": { + "button": "Konfigurer", "description_listen": "Lyt til et emne", "description_publish": "Udsend en pakke", "listening_to": "Lytter til", @@ -2008,11 +2015,23 @@ }, "history": { "period": "Periode", + "ranges": { + "last_week": "Sidste uge", + "this_week": "Denne uge", + "today": "I dag", + "yesterday": "I går" + }, "showing_entries": "Viser poster for" }, "logbook": { "entries_not_found": "Der blev ikke fundet nogen logbogsposter.", "period": "Periode", + "ranges": { + "last_week": "Sidste uge", + "this_week": "Denne uge", + "today": "I dag", + "yesterday": "I går" + }, "showing_entries": "Viser poster for" }, "lovelace": { diff --git a/translations/frontend/nb.json b/translations/frontend/nb.json index 9bc5132250..ce2dfe7d53 100644 --- a/translations/frontend/nb.json +++ b/translations/frontend/nb.json @@ -506,6 +506,11 @@ "clear": "Tøm", "show_areas": "Vis områder" }, + "date-range-picker": { + "end_date": "Sluttdato", + "select": "Velg", + "start_date": "Startdato" + }, "device-picker": { "clear": "Tøm", "device": "Enhet", @@ -695,6 +700,7 @@ "zha_device_info": { "buttons": { "add": "Legg til enheter via denne enheten", + "clusters": "Behandle Clusters", "reconfigure": "Rekonfigurer enhet", "remove": "Fjern enhet", "zigbee_information": "Zigbee-enhetssignatur" @@ -1558,6 +1564,7 @@ } }, "mqtt": { + "button": "Konfigurer", "description_listen": "Lytt til et emne", "description_publish": "Publiser en pakke", "listening_to": "Lytter til", @@ -1678,11 +1685,11 @@ "core": "Last inn lokasjon og spesialtilpassinger på nytt", "group": "Last inn grupper på nytt", "heading": "YAML -Konfigurasjon lastes på nytt", - "input_boolean": "Last input booleans på nytt", - "input_datetime": "Last input date på nytt", - "input_number": "Las input numbers på nytt", - "input_select": "Last input selects på nytt ", - "input_text": "Last input texts på nytt", + "input_boolean": "Last inn bolsk inndata på nytt", + "input_datetime": "Last inn dato inndata på nytt", + "input_number": "Last inn nummer inndata på nytt", + "input_select": "Last inn valg inndata på nytt ", + "input_text": "Last inn tekst inndata på nytt", "introduction": "Noen deler av Home Assistant kan laste inn uten å kreve omstart. Hvis du trykker last på nytt, vil du bytte den nåværende konfigurasjonen med den nye.", "person": "Last inn personer på nytt", "scene": "Last inn scener på nytt", @@ -1742,14 +1749,14 @@ "system": "" } }, - "users_privileges_note": "Brukere-gruppen er et pågående arbeid. Brukeren kan ikke administrere forekomsten via brukergrensesnittet. Vi overvåker fortsatt alle API-endepunkter for administrasjonsadministrasjon for å sikre at de begrenser tilgangen til administratorer på riktig måte." + "users_privileges_note": "Brukere-gruppen er et pågående arbeid. Brukeren kan ikke administrere forekomsten via brukergrensesnittet. Vi reviderer fortsatt alle API-endepunkter for å sikre at de begrenser tilgangen til administratorer på riktig måte." }, "zha": { "add_device_page": { "discovered_text": "Enheter vises her når de er oppdaget.", "discovery_text": "Oppdagede enheter vises her. Følg instruksjonene for enheten(e) og sett enheten(e) i paringsmodus.", "header": "Zigbee Home Automation - Legg til enheter", - "no_devices_found": "Ingen enheter er funnet, sørg for at de er i paringsmodus og holde dem våken mens du oppdager kjører.", + "no_devices_found": "Ingen enheter ble funnet, sørg for at de er i paringsmodus og holde dem våken mens oppdagelse pågår.", "pairing_mode": "Kontroller at enhetene er i paringsmodus. Sjekk instruksjonene til enheten om hvordan du gjør dette.", "search_again": "Søk på nytt", "spinner": "Søker etter ZHA Zigbee-enheter..." @@ -2035,11 +2042,23 @@ }, "history": { "period": "Periode", + "ranges": { + "last_week": "Forrige uke", + "this_week": "Denne uken", + "today": "I dag", + "yesterday": "I går" + }, "showing_entries": "Viser oppføringer for" }, "logbook": { "entries_not_found": "Finner ingen loggbokoppføringer.", "period": "Periode", + "ranges": { + "last_week": "Forrige uke", + "this_week": "Denne uken", + "today": "I dag", + "yesterday": "I går" + }, "showing_entries": "Viser oppføringer for" }, "lovelace": {