mirror of
https://github.com/home-assistant/frontend.git
synced 2025-04-24 21:37:21 +00:00
Add humidifier entity integration
This commit is contained in:
parent
362236d7df
commit
ec5d7508c9
@ -37,6 +37,7 @@ export const DOMAINS_WITH_MORE_INFO = [
|
||||
"fan",
|
||||
"group",
|
||||
"history_graph",
|
||||
"humidifier",
|
||||
"input_datetime",
|
||||
"light",
|
||||
"lock",
|
||||
|
@ -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",
|
||||
|
@ -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"],
|
||||
|
142
src/components/ha-humidifier-control.js
Normal file
142
src/components/ha-humidifier-control.js
Normal file
@ -0,0 +1,142 @@
|
||||
import "@polymer/iron-flex-layout/iron-flex-layout-classes";
|
||||
import "./ha-icon-button";
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import { EventsMixin } from "../mixins/events-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin EventsMixin
|
||||
*/
|
||||
class HaHumidifierControl extends EventsMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style include="iron-flex iron-flex-alignment"></style>
|
||||
<style>
|
||||
/* local DOM styles go here */
|
||||
:host {
|
||||
@apply --layout-flex;
|
||||
@apply --layout-horizontal;
|
||||
@apply --layout-justified;
|
||||
}
|
||||
.in-flux#humidity {
|
||||
color: var(--google-red-500);
|
||||
}
|
||||
#humidity {
|
||||
@apply --layout-self-center;
|
||||
font-size: 200%;
|
||||
direction: ltr;
|
||||
}
|
||||
.control-buttons {
|
||||
font-size: 200%;
|
||||
text-align: right;
|
||||
}
|
||||
ha-icon-button {
|
||||
--mdc-icon-size: 32px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- local DOM goes here -->
|
||||
<div id="humidity">[[value]] [[units]]</div>
|
||||
<div class="control-buttons">
|
||||
<div>
|
||||
<ha-icon-button
|
||||
icon="hass:chevron-up"
|
||||
on-click="incrementValue"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
<div>
|
||||
<ha-icon-button
|
||||
icon="hass:chevron-down"
|
||||
on-click="decrementValue"
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
value: {
|
||||
type: Number,
|
||||
observer: "valueChanged",
|
||||
},
|
||||
units: {
|
||||
type: String,
|
||||
},
|
||||
min: {
|
||||
type: Number,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
value: 1,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
humidityStateInFlux(inFlux) {
|
||||
this.$.humidity.classList.toggle("in-flux", inFlux);
|
||||
}
|
||||
|
||||
_round(val) {
|
||||
// round value to precision derived from step
|
||||
// insired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
|
||||
const s = this.step.toString().split(".");
|
||||
return s[1] ? parseFloat(val.toFixed(s[1].length)) : Math.round(val);
|
||||
}
|
||||
|
||||
incrementValue() {
|
||||
const newval = this._round(this.value + this.step);
|
||||
if (this.value < this.max) {
|
||||
this.last_changed = Date.now();
|
||||
this.humidityStateInFlux(true);
|
||||
}
|
||||
if (newval <= this.max) {
|
||||
// If no initial target_temp
|
||||
// this forces control to start
|
||||
// from the min configured instead of 0
|
||||
if (newval <= this.min) {
|
||||
this.value = this.min;
|
||||
} else {
|
||||
this.value = newval;
|
||||
}
|
||||
} else {
|
||||
this.value = this.max;
|
||||
}
|
||||
}
|
||||
|
||||
decrementValue() {
|
||||
const newval = this._round(this.value - this.step);
|
||||
if (this.value > this.min) {
|
||||
this.last_changed = Date.now();
|
||||
this.humidityStateInFlux(true);
|
||||
}
|
||||
if (newval >= this.min) {
|
||||
this.value = newval;
|
||||
} else {
|
||||
this.value = this.min;
|
||||
}
|
||||
}
|
||||
|
||||
valueChanged() {
|
||||
// when the last_changed timestamp is changed,
|
||||
// trigger a potential event fire in
|
||||
// the future, as long as last changed is far enough in the
|
||||
// past.
|
||||
if (this.last_changed) {
|
||||
window.setTimeout(() => {
|
||||
const now = Date.now();
|
||||
if (now - this.last_changed >= 2000) {
|
||||
this.fire("change");
|
||||
this.humidityStateInFlux(false);
|
||||
this.last_changed = null;
|
||||
}
|
||||
}, 2010);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("ha-humidifier-control", HaHumidifierControl);
|
83
src/components/ha-humidifier-state.js
Normal file
83
src/components/ha-humidifier-state.js
Normal file
@ -0,0 +1,83 @@
|
||||
import { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||
/* eslint-plugin-disable lit */
|
||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||
import LocalizeMixin from "../mixins/localize-mixin";
|
||||
|
||||
/*
|
||||
* @appliesMixin LocalizeMixin
|
||||
*/
|
||||
class HaHumidifierState extends LocalizeMixin(PolymerElement) {
|
||||
static get template() {
|
||||
return html`
|
||||
<style>
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.target {
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.current {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.state-label {
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.unit {
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="target">
|
||||
<template is="dom-if" if="[[_hasKnownState(stateObj.state)]]">
|
||||
<span class="state-label">
|
||||
[[_localizeState(localize, stateObj.state)]]
|
||||
<template is="dom-if" if="[[_renderMode(stateObj.attributes)]]">
|
||||
- [[_localizeMode(localize, stateObj.attributes.mode)]]
|
||||
</template>
|
||||
</span>
|
||||
</template>
|
||||
<div class="unit">[[computeTarget(stateObj.attributes.humidity)]]</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get properties() {
|
||||
return {
|
||||
stateObj: Object,
|
||||
};
|
||||
}
|
||||
|
||||
computeTarget(humidity) {
|
||||
if (humidity != null) {
|
||||
return `${humidity} %`;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
_hasKnownState(state) {
|
||||
return state !== "unknown";
|
||||
}
|
||||
|
||||
_localizeState(localize, state) {
|
||||
return localize(`state.default.${state}`) || state;
|
||||
}
|
||||
|
||||
_localizeMode(localize, mode) {
|
||||
return localize(`state_attributes.humidifier.mode.${mode}`) || mode;
|
||||
}
|
||||
|
||||
_renderMode(attributes) {
|
||||
return attributes.mode;
|
||||
}
|
||||
}
|
||||
customElements.define("ha-humidifier-state", HaHumidifierState);
|
@ -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";
|
||||
|
@ -5,13 +5,14 @@ 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",
|
||||
];
|
||||
|
||||
export interface LineChartState {
|
||||
@ -221,6 +222,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) {
|
||||
|
19
src/data/humidifier.ts
Normal file
19
src/data/humidifier.ts
Normal 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";
|
@ -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";
|
||||
|
219
src/dialogs/more-info/controls/more-info-humidifier.ts
Normal file
219
src/dialogs/more-info/controls/more-info-humidifier.ts
Normal file
@ -0,0 +1,219 @@
|
||||
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-humidifier-control";
|
||||
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-preset_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);
|
397
src/panels/lovelace/cards/hui-humidifier-card.ts
Normal file
397
src/panels/lovelace/cards/hui-humidifier-card.ts
Normal file
@ -0,0 +1,397 @@
|
||||
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 { classMap } from "lit-html/directives/class-map";
|
||||
import { UNIT_F } from "../../../common/const";
|
||||
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 } from "../../../data/entity";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { actionHandler } from "../common/directives/action-handler-directive";
|
||||
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 | 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 =
|
||||
stateObj.state === UNAVAILABLE
|
||||
? 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"
|
||||
>
|
||||
${
|
||||
stateObj.state === UNAVAILABLE
|
||||
? this.hass.localize("state.default.unavailable")
|
||||
: 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
|
||||
? 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 | [number, number] {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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",
|
||||
|
@ -6,6 +6,7 @@ import "../cards/hui-entity-card";
|
||||
import "../cards/hui-glance-card";
|
||||
import "../cards/hui-history-graph-card";
|
||||
import "../cards/hui-horizontal-stack-card";
|
||||
import "../cards/hui-humidifier-card";
|
||||
import "../cards/hui-light-card";
|
||||
import "../cards/hui-sensor-card";
|
||||
import "../cards/hui-thermostat-card";
|
||||
@ -24,6 +25,7 @@ const ALWAYS_LOADED_TYPES = new Set([
|
||||
"glance",
|
||||
"history-graph",
|
||||
"horizontal-stack",
|
||||
"humidifier",
|
||||
"light",
|
||||
"sensor",
|
||||
"thermostat",
|
||||
|
@ -24,6 +24,7 @@ const LAZY_LOAD_TYPES = {
|
||||
"climate-entity": () => import("../entity-rows/hui-climate-entity-row"),
|
||||
"cover-entity": () => import("../entity-rows/hui-cover-entity-row"),
|
||||
"group-entity": () => import("../entity-rows/hui-group-entity-row"),
|
||||
"humidifier-entity": () => import("../entity-rows/hui-humidifier-entity-row"),
|
||||
"input-datetime-entity": () =>
|
||||
import("../entity-rows/hui-input-datetime-entity-row"),
|
||||
"input-number-entity": () =>
|
||||
@ -51,6 +52,7 @@ const DOMAIN_TO_ELEMENT_TYPE = {
|
||||
cover: "cover",
|
||||
fan: "toggle",
|
||||
group: "group",
|
||||
humidifier: "humidifier",
|
||||
input_boolean: "toggle",
|
||||
input_number: "input-number",
|
||||
input_select: "input-select",
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -29,6 +29,10 @@ export const coreCards: Card[] = [
|
||||
type: "history-graph",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "humidifier",
|
||||
showElement: true,
|
||||
},
|
||||
{
|
||||
type: "light",
|
||||
showElement: true,
|
||||
|
74
src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts
Normal file
74
src/panels/lovelace/entity-rows/hui-humidifier-entity-row.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import {
|
||||
css,
|
||||
CSSResult,
|
||||
customElement,
|
||||
html,
|
||||
LitElement,
|
||||
property,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit-element";
|
||||
import "../../../components/ha-humidifier-state";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import "../components/hui-generic-entity-row";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import { EntityConfig, LovelaceRow } from "./types";
|
||||
|
||||
@customElement("hui-humidifier-entity-row")
|
||||
class HuiHumidifierEntityRow extends LitElement implements LovelaceRow {
|
||||
@property() public hass?: HomeAssistant;
|
||||
|
||||
@property() private _config?: EntityConfig;
|
||||
|
||||
public setConfig(config: EntityConfig): void {
|
||||
if (!config || !config.entity) {
|
||||
throw new Error("Invalid Configuration: 'entity' required");
|
||||
}
|
||||
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return hasConfigOrEntityChanged(this, changedProps);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (!this.hass || !this._config) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
<hui-warning>
|
||||
${createEntityNotFoundWarning(this.hass, this._config.entity)}
|
||||
</hui-warning>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<hui-generic-entity-row .hass=${this.hass} .config=${this._config}>
|
||||
<ha-humidifier-state
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-humidifier-state>
|
||||
</hui-generic-entity-row>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
ha-humidifier-state {
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-humidifier-entity-row": HuiHumidifierEntityRow;
|
||||
}
|
||||
}
|
@ -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,11 @@
|
||||
"forward": "Forward",
|
||||
"reverse": "Reverse"
|
||||
},
|
||||
"humidifier": {
|
||||
"mode": "Mode",
|
||||
"target_humidity_entity": "{name} target humidity",
|
||||
"on_entity": "{name} on"
|
||||
},
|
||||
"light": {
|
||||
"brightness": "Brightness",
|
||||
"color_temperature": "Color temperature",
|
||||
@ -1856,6 +1874,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."
|
||||
|
@ -89,7 +89,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 +100,7 @@ hassAttributeUtil.LOGIC_STATE_ATTRIBUTES = hassAttributeUtil.LOGIC_STATE_ATTRIBU
|
||||
"cover",
|
||||
"climate",
|
||||
"fan",
|
||||
"humidifier",
|
||||
"group",
|
||||
"water_heater",
|
||||
],
|
||||
|
Loading…
x
Reference in New Issue
Block a user