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`
+
+
+ `;
+
+ return html`
+
+
+
+
+
+ `;
+ }
+
+ 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": {