Merge pull request #6214 from home-assistant/dev

This commit is contained in:
Paulus Schoutsen 2020-06-23 09:12:55 -07:00 committed by GitHub
commit 5268afabdb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 913 additions and 61 deletions

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup( setup(
name="home-assistant-frontend", name="home-assistant-frontend",
version="20200622.0", version="20200623.0",
description="The Home Assistant frontend", description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer", url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors", author="The Home Assistant Authors",

View File

@ -37,6 +37,7 @@ export const DOMAINS_WITH_MORE_INFO = [
"fan", "fan",
"group", "group",
"history_graph", "history_graph",
"humidifier",
"input_datetime", "input_datetime",
"light", "light",
"lock", "lock",
@ -79,6 +80,7 @@ export const DOMAINS_TOGGLE = new Set([
"switch", "switch",
"group", "group",
"automation", "automation",
"humidifier",
]); ]);
/** Temperature units. */ /** Temperature units. */

View File

@ -55,6 +55,12 @@ export const computeStateDisplay = (
return formatDateTime(date, language); return formatDateTime(date, language);
} }
if (domain === "humidifier") {
if (stateObj.state === "on" && stateObj.attributes.humidity) {
return `${stateObj.attributes.humidity}%`;
}
}
return ( return (
// Return device class translation // Return device class translation
(stateObj.attributes.device_class && (stateObj.attributes.device_class &&

View File

@ -22,6 +22,7 @@ const fixedIcons = {
history_graph: "hass:chart-line", history_graph: "hass:chart-line",
homeassistant: "hass:home-assistant", homeassistant: "hass:home-assistant",
homekit: "hass:home-automation", homekit: "hass:home-automation",
humidifier: "hass:air-humidifier",
image_processing: "hass:image-filter-frames", image_processing: "hass:image-filter-frames",
input_boolean: "hass:toggle-switch-outline", input_boolean: "hass:toggle-switch-outline",
input_datetime: "hass:calendar-clock", input_datetime: "hass:calendar-clock",

View File

@ -8,6 +8,7 @@ export const iconColorCSS = css`
ha-icon[data-domain="camera"][data-state="streaming"], ha-icon[data-domain="camera"][data-state="streaming"],
ha-icon[data-domain="cover"][data-state="open"], ha-icon[data-domain="cover"][data-state="open"],
ha-icon[data-domain="fan"][data-state="on"], 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="light"][data-state="on"],
ha-icon[data-domain="input_boolean"][data-state="on"], ha-icon[data-domain="input_boolean"][data-state="on"],
ha-icon[data-domain="lock"][data-state="unlocked"], ha-icon[data-domain="lock"][data-state="unlocked"],

View File

@ -22,7 +22,7 @@ const isOn = (stateObj?: HassEntity) =>
!STATES_OFF.includes(stateObj.state) && !STATES_OFF.includes(stateObj.state) &&
!UNAVAILABLE_STATES.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 // hass is not a property so that we only re-render on stateObj changes
public hass?: HomeAssistant; public hass?: HomeAssistant;

View File

@ -262,6 +262,28 @@ class StateHistoryChartLine extends LocalizeMixin(PolymerElement) {
pushData(new Date(state.last_changed), series); 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 { } else {
// Only disable interpolation for sensors // Only disable interpolation for sensors
const isStep = domain === "sensor"; const isStep = domain === "sensor";

View File

@ -5,13 +5,15 @@ import { computeStateName } from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize"; import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types"; 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 = [ const LINE_ATTRIBUTES_TO_KEEP = [
"temperature", "temperature",
"current_temperature", "current_temperature",
"target_temp_low", "target_temp_low",
"target_temp_high", "target_temp_high",
"hvac_action", "hvac_action",
"humidity",
"mode",
]; ];
export interface LineChartState { export interface LineChartState {
@ -224,6 +226,8 @@ export const computeHistory = (
unit = hass.config.unit_system.temperature; unit = hass.config.unit_system.temperature;
} else if (computeStateDomain(stateInfo[0]) === "water_heater") { } else if (computeStateDomain(stateInfo[0]) === "water_heater") {
unit = hass.config.unit_system.temperature; unit = hass.config.unit_system.temperature;
} else if (computeStateDomain(stateInfo[0]) === "humidifier") {
unit = "%";
} }
if (!unit) { if (!unit) {

19
src/data/humidifier.ts Normal file
View File

@ -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";

View File

@ -14,6 +14,7 @@ import "./more-info-default";
import "./more-info-fan"; import "./more-info-fan";
import "./more-info-group"; import "./more-info-group";
import "./more-info-history_graph"; import "./more-info-history_graph";
import "./more-info-humidifier";
import "./more-info-input_datetime"; import "./more-info-input_datetime";
import "./more-info-light"; import "./more-info-light";
import "./more-info-lock"; import "./more-info-lock";

View File

@ -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`
<div
class=${classMap({
"has-modes": supportModes,
})}
>
<div class="container-humidity">
<div>${hass.localize("ui.card.humidifier.humidity")}</div>
<div class="single-row">
<div class="target-humidity">
${stateObj.attributes.humidity} %
</div>
<ha-paper-slider
class="humidity"
step="1"
pin
ignore-bar-touch
dir=${rtlDirection}
.min=${stateObj.attributes.min_humidity}
.max=${stateObj.attributes.max_humidity}
.secondaryProgress=${stateObj.attributes.max_humidity}
.value=${stateObj.attributes.humidity}
@change=${this._targetHumiditySliderChanged}
>
</ha-paper-slider>
</div>
</div>
${supportModes
? html`
<div class="container-modes">
<ha-paper-dropdown-menu
label-float
dynamic-align
.label=${hass.localize("ui.card.humidifier.mode")}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-name"
.selected=${stateObj.attributes.mode}
@selected-changed=${this._handleModeChanged}
>
${stateObj.attributes.available_modes!.map(
(mode) => html`
<paper-item item-name=${mode}>
${hass.localize(
`state_attributes.humidifier.mode.${mode}`
) || mode}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>
</div>
`
: ""}
</div>
`;
}
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);

View File

@ -22,6 +22,10 @@ export class HomeAssistantAppEl extends HassElement {
private _haVersion?: string; private _haVersion?: string;
private _hiddenTimeout?: number;
private _visiblePromiseResolve?: () => void;
protected render() { protected render() {
const hass = this.hass; const hass = this.hass;
@ -71,6 +75,12 @@ export class HomeAssistantAppEl extends HassElement {
super.hassConnected(); super.hassConnected();
// @ts-ignore // @ts-ignore
this._loadHassTranslations(this.hass!.language, "state"); this._loadHassTranslations(this.hass!.language, "state");
document.addEventListener(
"visibilitychange",
() => this.__handleVisibilityChange(),
false
);
} }
protected hassReconnected() { protected hassReconnected() {
@ -137,6 +147,33 @@ export class HomeAssistantAppEl extends HassElement {
? route.path.substr(1) ? route.path.substr(1)
: route.path.substr(1, dividerPos - 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 { declare global {

View File

@ -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<LovelaceCardEditor> {
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`
<hui-warning>
${createEntityNotFoundWarning(this.hass, this._config.entity)}
</hui-warning>
`;
}
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` <round-slider disabled="true"></round-slider> `
: html`
<round-slider
.value=${targetHumidity}
.min=${stateObj.attributes.min_humidity}
.max=${stateObj.attributes.max_humidity}
.rtl=${rtlDirection === "rtl"}
.step="1"
@value-changing=${this._dragEvent}
@value-changed=${this._setHumidity}
></round-slider>
`;
const setValues = svg`
<svg viewBox="0 0 40 20">
<text
x="50%"
dx="1"
y="60%"
text-anchor="middle"
style="font-size: 13px;"
class="set-value"
>
${
UNAVAILABLE_STATES.includes(stateObj.state) ||
this._setHum === undefined ||
this._setHum === null
? ""
: svg`
${this._setHum.toFixed()}
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
%
</tspan>
`
}
</text>
</svg>
<svg id="set-values">
<g>
<text
dy="22"
text-anchor="middle"
id="set-mode"
>
${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}
`
: ""
}
</text>
</g>
</svg>
`;
return html`
<ha-card>
<ha-icon-button
icon="hass:dots-vertical"
class="more-info"
@click=${this._handleMoreInfo}
tabindex="0"
></ha-icon-button>
<div class="content">
<div id="controls">
<div id="slider">
${slider}
<div id="slider-center">
<div id="humidity">
${setValues}
</div>
</div>
</div>
</div>
<div id="info">
${name}
</div>
</div>
</ha-card>
`;
}
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;
}
}

View File

@ -133,6 +133,12 @@ export interface GlanceCardConfig extends LovelaceCardConfig {
state_color?: boolean; state_color?: boolean;
} }
export interface HumidifierCardConfig extends LovelaceCardConfig {
entity: string;
theme?: string;
name?: string;
}
export interface IframeCardConfig extends LovelaceCardConfig { export interface IframeCardConfig extends LovelaceCardConfig {
aspect_ratio?: string; aspect_ratio?: string;
title?: string; title?: string;

View File

@ -37,6 +37,7 @@ import { GroupEntity, HomeAssistant } from "../../../types";
import { import {
AlarmPanelCardConfig, AlarmPanelCardConfig,
EntitiesCardConfig, EntitiesCardConfig,
HumidifierCardConfig,
LightCardConfig, LightCardConfig,
PictureEntityCardConfig, PictureEntityCardConfig,
ThermostatCardConfig, ThermostatCardConfig,
@ -150,6 +151,12 @@ export const computeCards = (
refresh_interval: stateObj.attributes.refresh, refresh_interval: stateObj.attributes.refresh,
}; };
cards.push(cardConfig); cards.push(cardConfig);
} else if (domain === "humidifier") {
const cardConfig: HumidifierCardConfig = {
type: "humidifier",
entity: entityId,
};
cards.push(cardConfig);
} else if (domain === "light" && single) { } else if (domain === "light" && single) {
const cardConfig: LightCardConfig = { const cardConfig: LightCardConfig = {
type: "light", type: "light",

View File

@ -38,6 +38,7 @@ const LAZY_LOAD_TYPES = {
"empty-state": () => import("../cards/hui-empty-state-card"), "empty-state": () => import("../cards/hui-empty-state-card"),
starting: () => import("../cards/hui-starting-card"), starting: () => import("../cards/hui-starting-card"),
"entity-filter": () => import("../cards/hui-entity-filter-card"), "entity-filter": () => import("../cards/hui-entity-filter-card"),
humidifier: () => import("../cards/hui-humidifier-card"),
"media-control": () => import("../cards/hui-media-control-card"), "media-control": () => import("../cards/hui-media-control-card"),
"picture-elements": () => import("../cards/hui-picture-elements-card"), "picture-elements": () => import("../cards/hui-picture-elements-card"),
"picture-entity": () => import("../cards/hui-picture-entity-card"), "picture-entity": () => import("../cards/hui-picture-entity-card"),

View File

@ -51,6 +51,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
cover: "cover", cover: "cover",
fan: "toggle", fan: "toggle",
group: "group", group: "group",
humidifier: "toggle",
input_boolean: "toggle", input_boolean: "toggle",
input_number: "input-number", input_number: "input-number",
input_select: "input-select", input_select: "input-select",

View File

@ -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}
<div class="card-config">
<ha-entity-picker
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.entity"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.required"
)})"
.hass=${this.hass}
.value="${this._entity}"
.configValue=${"entity"}
.includeDomains=${includeDomains}
@change="${this._valueChanged}"
allow-custom-entity
></ha-entity-picker>
<paper-input
.label="${this.hass.localize(
"ui.panel.lovelace.editor.card.generic.name"
)} (${this.hass.localize(
"ui.panel.lovelace.editor.card.config.optional"
)})"
.value="${this._name}"
.configValue="${"name"}"
@value-changed="${this._valueChanged}"
></paper-input>
<hui-theme-select-editor
.hass=${this.hass}
.value="${this._theme}"
.configValue="${"theme"}"
@value-changed="${this._valueChanged}"
></hui-theme-select-editor>
</div>
`;
}
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;
}
}

View File

@ -29,6 +29,10 @@ export const coreCards: Card[] = [
type: "history-graph", type: "history-graph",
showElement: true, showElement: true,
}, },
{
type: "humidifier",
showElement: true,
},
{ {
type: "light", type: "light",
showElement: true, showElement: true,

View File

@ -12,7 +12,6 @@ import SidebarMixin from "./sidebar-mixin";
import ThemesMixin from "./themes-mixin"; import ThemesMixin from "./themes-mixin";
import TranslationsMixin from "./translations-mixin"; import TranslationsMixin from "./translations-mixin";
import { urlSyncMixin } from "./url-sync-mixin"; import { urlSyncMixin } from "./url-sync-mixin";
import { suspendMixin } from "./suspend-mixin";
const ext = <T extends Constructor>(baseClass: T, mixins): T => const ext = <T extends Constructor>(baseClass: T, mixins): T =>
mixins.reduceRight((base, mixin) => mixin(base), baseClass); mixins.reduceRight((base, mixin) => mixin(base), baseClass);
@ -25,7 +24,6 @@ export class HassElement extends ext(HassBaseEl, [
SidebarMixin, SidebarMixin,
DisconnectToastMixin, DisconnectToastMixin,
connectionMixin, connectionMixin,
suspendMixin,
NotificationMixin, NotificationMixin,
dialogManagerMixin, dialogManagerMixin,
urlSyncMixin, urlSyncMixin,

View File

@ -1,48 +0,0 @@
import { Constructor } from "../types";
import { HassBaseEl } from "./hass-base-mixin";
export const suspendMixin = <T extends Constructor<HassBaseEl>>(
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;
}
}
}
};

View File

@ -44,6 +44,19 @@
"idle": "Idle", "idle": "Idle",
"fan": "Fan" "fan": "Fan"
} }
},
"humidifier": {
"mode": {
"normal": "Normal",
"eco": "Eco",
"away": "Away",
"boost": "Boost",
"comfort": "Comfort",
"home": "Home",
"sleep": "Sleep",
"auto": "Auto",
"baby": "Baby"
}
} }
}, },
"state_badge": { "state_badge": {
@ -146,6 +159,12 @@
"forward": "Forward", "forward": "Forward",
"reverse": "Reverse" "reverse": "Reverse"
}, },
"humidifier": {
"humidity": "Target humidity",
"mode": "Mode",
"target_humidity_entity": "{name} target humidity",
"on_entity": "{name} on"
},
"light": { "light": {
"brightness": "Brightness", "brightness": "Brightness",
"color_temperature": "Color temperature", "color_temperature": "Color temperature",
@ -1935,6 +1954,10 @@
"name": "Horizontal Stack", "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." "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": { "iframe": {
"name": "Webpage", "name": "Webpage",
"description": "The Webpage card allows you to embed your favorite webpage right into Home Assistant." "description": "The Webpage card allows you to embed your favorite webpage right into Home Assistant."

View File

@ -37,6 +37,7 @@ hassAttributeUtil.DOMAIN_DEVICE_CLASS = {
"shutter", "shutter",
"window", "window",
], ],
humidifier: ["dehumidifier", "humidifier"],
sensor: [ sensor: [
"battery", "battery",
"humidity", "humidity",
@ -89,7 +90,7 @@ hassAttributeUtil.LOGIC_STATE_ATTRIBUTES = hassAttributeUtil.LOGIC_STATE_ATTRIBU
type: "array", type: "array",
options: hassAttributeUtil.DOMAIN_DEVICE_CLASS, options: hassAttributeUtil.DOMAIN_DEVICE_CLASS,
description: "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" }, hidden: { type: "boolean", description: "Hide from UI" },
assumed_state: { assumed_state: {
@ -100,6 +101,7 @@ hassAttributeUtil.LOGIC_STATE_ATTRIBUTES = hassAttributeUtil.LOGIC_STATE_ATTRIBU
"cover", "cover",
"climate", "climate",
"fan", "fan",
"humidifier",
"group", "group",
"water_heater", "water_heater",
], ],

View File

@ -506,6 +506,11 @@
"clear": "Ryd", "clear": "Ryd",
"show_areas": "Vis områder" "show_areas": "Vis områder"
}, },
"date-range-picker": {
"end_date": "Slutdato",
"select": "Vælg",
"start_date": "Startdato"
},
"device-picker": { "device-picker": {
"clear": "Ryd", "clear": "Ryd",
"device": "Enhed", "device": "Enhed",
@ -695,6 +700,7 @@
"zha_device_info": { "zha_device_info": {
"buttons": { "buttons": {
"add": "Tilføj enheder", "add": "Tilføj enheder",
"clusters": "Administrer klynger",
"reconfigure": "Genkonfigurer enhed", "reconfigure": "Genkonfigurer enhed",
"remove": "Fjern enhed", "remove": "Fjern enhed",
"zigbee_information": "Zigbee-oplysninger" "zigbee_information": "Zigbee-oplysninger"
@ -1558,6 +1564,7 @@
} }
}, },
"mqtt": { "mqtt": {
"button": "Konfigurer",
"description_listen": "Lyt til et emne", "description_listen": "Lyt til et emne",
"description_publish": "Udsend en pakke", "description_publish": "Udsend en pakke",
"listening_to": "Lytter til", "listening_to": "Lytter til",
@ -2008,11 +2015,23 @@
}, },
"history": { "history": {
"period": "Periode", "period": "Periode",
"ranges": {
"last_week": "Sidste uge",
"this_week": "Denne uge",
"today": "I dag",
"yesterday": "I går"
},
"showing_entries": "Viser poster for" "showing_entries": "Viser poster for"
}, },
"logbook": { "logbook": {
"entries_not_found": "Der blev ikke fundet nogen logbogsposter.", "entries_not_found": "Der blev ikke fundet nogen logbogsposter.",
"period": "Periode", "period": "Periode",
"ranges": {
"last_week": "Sidste uge",
"this_week": "Denne uge",
"today": "I dag",
"yesterday": "I går"
},
"showing_entries": "Viser poster for" "showing_entries": "Viser poster for"
}, },
"lovelace": { "lovelace": {

View File

@ -506,6 +506,11 @@
"clear": "Tøm", "clear": "Tøm",
"show_areas": "Vis områder" "show_areas": "Vis områder"
}, },
"date-range-picker": {
"end_date": "Sluttdato",
"select": "Velg",
"start_date": "Startdato"
},
"device-picker": { "device-picker": {
"clear": "Tøm", "clear": "Tøm",
"device": "Enhet", "device": "Enhet",
@ -695,6 +700,7 @@
"zha_device_info": { "zha_device_info": {
"buttons": { "buttons": {
"add": "Legg til enheter via denne enheten", "add": "Legg til enheter via denne enheten",
"clusters": "Behandle Clusters",
"reconfigure": "Rekonfigurer enhet", "reconfigure": "Rekonfigurer enhet",
"remove": "Fjern enhet", "remove": "Fjern enhet",
"zigbee_information": "Zigbee-enhetssignatur" "zigbee_information": "Zigbee-enhetssignatur"
@ -1558,6 +1564,7 @@
} }
}, },
"mqtt": { "mqtt": {
"button": "Konfigurer",
"description_listen": "Lytt til et emne", "description_listen": "Lytt til et emne",
"description_publish": "Publiser en pakke", "description_publish": "Publiser en pakke",
"listening_to": "Lytter til", "listening_to": "Lytter til",
@ -1678,11 +1685,11 @@
"core": "Last inn lokasjon og spesialtilpassinger på nytt", "core": "Last inn lokasjon og spesialtilpassinger på nytt",
"group": "Last inn grupper på nytt", "group": "Last inn grupper på nytt",
"heading": "YAML -Konfigurasjon lastes på nytt", "heading": "YAML -Konfigurasjon lastes på nytt",
"input_boolean": "Last input booleans på nytt", "input_boolean": "Last inn bolsk inndata på nytt",
"input_datetime": "Last input date på nytt", "input_datetime": "Last inn dato inndata på nytt",
"input_number": "Las input numbers på nytt", "input_number": "Last inn nummer inndata på nytt",
"input_select": "Last input selects på nytt ", "input_select": "Last inn valg inndata på nytt ",
"input_text": "Last input texts 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.", "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", "person": "Last inn personer på nytt",
"scene": "Last inn scener på nytt", "scene": "Last inn scener på nytt",
@ -1742,14 +1749,14 @@
"system": "" "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": { "zha": {
"add_device_page": { "add_device_page": {
"discovered_text": "Enheter vises her når de er oppdaget.", "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.", "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", "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.", "pairing_mode": "Kontroller at enhetene er i paringsmodus. Sjekk instruksjonene til enheten om hvordan du gjør dette.",
"search_again": "Søk på nytt", "search_again": "Søk på nytt",
"spinner": "Søker etter ZHA Zigbee-enheter..." "spinner": "Søker etter ZHA Zigbee-enheter..."
@ -2035,11 +2042,23 @@
}, },
"history": { "history": {
"period": "Periode", "period": "Periode",
"ranges": {
"last_week": "Forrige uke",
"this_week": "Denne uken",
"today": "I dag",
"yesterday": "I går"
},
"showing_entries": "Viser oppføringer for" "showing_entries": "Viser oppføringer for"
}, },
"logbook": { "logbook": {
"entries_not_found": "Finner ingen loggbokoppføringer.", "entries_not_found": "Finner ingen loggbokoppføringer.",
"period": "Periode", "period": "Periode",
"ranges": {
"last_week": "Forrige uke",
"this_week": "Denne uken",
"today": "I dag",
"yesterday": "I går"
},
"showing_entries": "Viser oppføringer for" "showing_entries": "Viser oppføringer for"
}, },
"lovelace": { "lovelace": {