Align humidifier thermostat card (#17054)

This commit is contained in:
Paul Bottein 2023-06-27 18:36:56 +02:00 committed by GitHub
parent 3b8ea5edbe
commit 343708cdaa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 233 additions and 177 deletions

View File

@ -169,12 +169,6 @@ export const computeStateDisplayFromEntityAttributes = (
} }
} }
if (domain === "humidifier") {
if (state === "on" && attributes.humidity) {
return `${attributes.humidity} %`;
}
}
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber` // `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
if ( if (
domain === "counter" || domain === "counter" ||

View File

@ -1,26 +1,30 @@
import { mdiDotsVertical } from "@mdi/js"; import { mdiDotsVertical, mdiPower, mdiWaterPercent } from "@mdi/js";
import "@thomasloven/round-slider"; import "@thomasloven/round-slider";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
PropertyValues, PropertyValues,
svg, css,
html,
nothing, nothing,
svg,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display"; import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name"; import { computeStateName } from "../../../common/entity/compute_state_name";
import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { stateColorCss } from "../../../common/entity/state_color";
import { formatNumber } from "../../../common/number/format_number"; import { formatNumber } from "../../../common/number/format_number";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
import "../../../components/ha-card"; import "../../../components/ha-card";
import type { HaCard } from "../../../components/ha-card";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import { isUnavailableState } from "../../../data/entity"; import { UNAVAILABLE, isUnavailableState } from "../../../data/entity";
import { HumidifierEntity } from "../../../data/humidifier"; import { HumidifierEntity } from "../../../data/humidifier";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { findEntities } from "../common/find-entities"; import { findEntities } from "../common/find-entities";
@ -60,8 +64,10 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
@state() private _setHum?: number; @state() private _setHum?: number;
@query("ha-card") private _card?: HaCard;
public getCardSize(): number { public getCardSize(): number {
return 6; return 7;
} }
public setConfig(config: HumidifierCardConfig): void { public setConfig(config: HumidifierCardConfig): void {
@ -98,19 +104,12 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
const setHumidity = this._setHum ? this._setHum : targetHumidity; const setHumidity = this._setHum ? this._setHum : targetHumidity;
const curHumidity =
stateObj.attributes.current_humidity !== null &&
Number.isFinite(Number(stateObj.attributes.current_humidity))
? stateObj.attributes.current_humidity
: undefined;
const rtlDirection = computeRTLDirection(this.hass); const rtlDirection = computeRTLDirection(this.hass);
const slider = isUnavailableState(stateObj.state) const slider = isUnavailableState(stateObj.state)
? html` <round-slider disabled="true"></round-slider> ` ? html` <round-slider disabled="true"></round-slider> `
: html` : html`
<round-slider <round-slider
class=${classMap({ "round-slider_off": stateObj.state === "off" })}
.value=${targetHumidity} .value=${targetHumidity}
.min=${stateObj.attributes.min_humidity} .min=${stateObj.attributes.min_humidity}
.max=${stateObj.attributes.max_humidity} .max=${stateObj.attributes.max_humidity}
@ -121,60 +120,89 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
></round-slider> ></round-slider>
`; `;
const setValues = html` const currentHumidity = svg`
<svg viewBox="0 0 30 20"> <svg viewBox="0 0 40 20">
<text x="50%" dx="1" y="73%" text-anchor="middle" id="set-values"> <text
${isUnavailableState(stateObj.state) || x="50%"
setHumidity === undefined || dx="1"
setHumidity === null y="60%"
? "" text-anchor="middle"
: svg` style="font-size: 13px;"
${formatNumber(setHumidity, this.hass.locale, { >
maximumFractionDigits: 0, ${
})} stateObj.state !== UNAVAILABLE &&
<tspan dx="-3" dy="-6.5" style="font-size: 4px;"> stateObj.attributes.current_humidity != null &&
% !isNaN(stateObj.attributes.current_humidity)
</tspan> ? svg`
`} ${formatNumber(
stateObj.attributes.current_humidity,
this.hass.locale
)}
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
%
</tspan>
`
: nothing
}
</text> </text>
</svg> </svg>
`; `;
const currentHumidity = html` const setValues = svg`
<svg viewBox="0 0 40 10" id="current_humidity"> <svg id="set-values">
<text x="50%" y="50%" text-anchor="middle" id="current-humidity"> <g>
${curHumidity <text text-anchor="middle" class="set-value">
? svg`${formatNumber(curHumidity, this.hass.locale)}` ${
: ""} stateObj.state !== UNAVAILABLE && setHumidity != null
</text> ? formatNumber(setHumidity, this.hass.locale, {
</svg> maximumFractionDigits: 0,
`; })
: nothing
const currentMode = html` }
<svg viewBox="0 0 40 10" id="mode"> </text>
<text x="50%" y="50%" text-anchor="middle" id="set-mode"> <text
${this.hass!.localize(`state.default.${stateObj.state}`)} dy="22"
${stateObj.attributes.mode && !isUnavailableState(stateObj.state) text-anchor="middle"
? html` id="set-mode"
- >
${computeAttributeValueDisplay( ${computeStateDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
this.hass.locale, this.hass.locale,
this.hass.config, this.hass.config,
this.hass.entities, this.hass.entities
"mode" )}
)} ${
` stateObj.state !== UNAVAILABLE && stateObj.attributes.mode
: ""} ? html`
</text> -
${computeAttributeValueDisplay(
this.hass.localize,
stateObj,
this.hass.locale,
this.hass.config,
this.hass.entities,
"mode"
)}
`
: nothing
}
</text>
</g>
</svg> </svg>
`; `;
return html` return html`
<ha-card> <ha-card
style=${styleMap({
"--mode-color": stateColorCss(stateObj),
})}
>
<ha-icon-button <ha-icon-button
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
.label=${this.hass!.localize(
"ui.panel.lovelace.cards.show_more_info"
)}
class="more-info" class="more-info"
@click=${this._handleMoreInfo} @click=${this._handleMoreInfo}
tabindex="0" tabindex="0"
@ -185,19 +213,35 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
<div id="slider"> <div id="slider">
${slider} ${slider}
<div id="slider-center"> <div id="slider-center">
<ha-icon-button <div id="humidity">${currentHumidity} ${setValues}</div>
class="toggle-button"
.disabled=${isUnavailableState(stateObj.state)}
@click=${this._toggle}
tabindex="0"
>
${setValues}
</ha-icon-button>
${currentHumidity} ${currentMode}
</div> </div>
</div> </div>
</div> </div>
<div id="info" .title=${name}>${name}</div> <div id="info" .title=${name}>
<div id="modes">
<ha-icon-button
class=${classMap({ "selected-icon": stateObj.state === "on" })}
@click=${this._turnOn}
tabindex="0"
.path=${mdiWaterPercent}
.label=${this.hass!.localize(
`component.humidifier.entity_component._.state.on`
)}
>
</ha-icon-button>
<ha-icon-button
class=${classMap({ "selected-icon": stateObj.state === "off" })}
@click=${this._turnOff}
tabindex="0"
.path=${mdiPower}
.label=${this.hass!.localize(
`component.humidifier.entity_component._.state.off`
)}
>
</ha-icon-button>
</div>
${name}
</div>
</div> </div>
</ha-card> </ha-card>
`; `;
@ -231,6 +275,15 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
) { ) {
applyThemesOnElement(this, this.hass.themes, 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._rescale_svg();
}
} }
public willUpdate(changedProps: PropertyValues) { public willUpdate(changedProps: PropertyValues) {
@ -250,6 +303,27 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
} }
} }
private _rescale_svg() {
// Set the viewbox of the SVG containing the set temperature to perfectly
// fit the text
// That way it will auto-scale correctly
// This is not done to the SVG containing the current temperature, because
// it should not be centered on the text, but only on the value
const card = this._card;
if (card) {
card.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 { private _getSetHum(stateObj: HassEntity): undefined | number {
if (isUnavailableState(stateObj.state)) { if (isUnavailableState(stateObj.state)) {
return undefined; return undefined;
@ -269,8 +343,14 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
}); });
} }
private _toggle(): void { private _turnOn(): void {
this.hass!.callService("humidifier", "toggle", { this.hass!.callService("humidifier", "turn_on", {
entity_id: this._config!.entity,
});
}
private _turnOff(): void {
this.hass!.callService("humidifier", "turn_off", {
entity_id: this._config!.entity, entity_id: this._config!.entity,
}); });
} }
@ -294,6 +374,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
--name-font-size: 1.2rem; --name-font-size: 1.2rem;
--brightness-font-size: 1.2rem; --brightness-font-size: 1.2rem;
--rail-border-color: transparent; --rail-border-color: transparent;
--mode-color: var(--state-inactive-color);
} }
.more-info { .more-info {
@ -301,11 +382,11 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
cursor: pointer; cursor: pointer;
top: 0; top: 0;
right: 0; right: 0;
inset-inline-end: 0px;
inset-inline-start: initial;
border-radius: 100%; border-radius: 100%;
color: var(--secondary-text-color); color: var(--secondary-text-color);
z-index: 25; z-index: 1;
inset-inline-start: initial;
inset-inline-end: 0;
direction: var(--direction); direction: var(--direction);
} }
@ -321,7 +402,6 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
justify-content: center; justify-content: center;
padding: 16px; padding: 16px;
position: relative; position: relative;
direction: ltr;
} }
#slider { #slider {
@ -334,13 +414,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
round-slider { round-slider {
--round-slider-path-color: var(--slider-track-color); --round-slider-path-color: var(--slider-track-color);
--round-slider-bar-color: var(--primary-color); --round-slider-bar-color: var(--mode-color);
padding-bottom: 10%;
}
.round-slider_off {
--round-slider-path-color: var(--slider-track-color);
--round-slider-bar-color: var(--disabled-text-color);
padding-bottom: 10%; padding-bottom: 10%;
} }
@ -354,47 +428,28 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
top: 20px; top: 20px;
text-align: center; text-align: center;
overflow-wrap: break-word; overflow-wrap: break-word;
pointer-events: none;
} }
#mode { #humidity {
max-width: 80%; position: absolute;
transform: translate(0, 250%); transform: translate(-50%, -50%);
width: 100%;
height: 50%;
top: 45%;
left: 50%;
direction: ltr;
} }
#set-values { #set-values {
font-size: 13px; max-width: 80%;
font-family: var(--paper-font-body1_-_font-family); transform: translate(0, -50%);
font-weight: var(--paper-font-body1_-_font-weight); font-size: 20px;
} }
#set-mode { #set-mode {
fill: var(--secondary-text-color); fill: var(--secondary-text-color);
font-size: 4px; font-size: 16px;
}
#current_humidity {
max-width: 80%;
transform: translate(0, 300%);
}
#current-humidity {
fill: var(--primary-text-color);
font-size: 5px;
}
.toggle-button {
color: var(--primary-text-color);
width: 75%;
height: auto;
position: absolute;
max-width: calc(100% - 40px);
box-sizing: border-box;
border-radius: 100%;
top: 39%;
left: 50%;
transform: translate(-50%, -50%);
--mdc-icon-button-size: 100%;
--mdc-icon-size: 100%;
} }
#info { #info {
@ -406,6 +461,16 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard {
font-size: var(--name-font-size); 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 { text {
fill: var(--primary-text-color); fill: var(--primary-text-color);
} }

View File

@ -166,16 +166,19 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
style="font-size: 13px;" style="font-size: 13px;"
> >
${ ${
stateObj.attributes.current_temperature !== null && stateObj.state !== UNAVAILABLE &&
stateObj.attributes.current_temperature != null &&
!isNaN(stateObj.attributes.current_temperature) !isNaN(stateObj.attributes.current_temperature)
? svg`${formatNumber( ? svg`
stateObj.attributes.current_temperature, ${formatNumber(
this.hass.locale stateObj.attributes.current_temperature,
)} this.hass.locale
<tspan dx="-3" dy="-6.5" style="font-size: 4px;"> )}
${this.hass.config.unit_system.temperature} <tspan dx="-3" dy="-6.5" style="font-size: 4px;">
</tspan>` ${this.hass.config.unit_system.temperature}
: "" </tspan>
`
: nothing
} }
</text> </text>
</svg> </svg>
@ -186,42 +189,14 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
<g> <g>
<text text-anchor="middle" class="set-value"> <text text-anchor="middle" class="set-value">
${ ${
stateObj.state === UNAVAILABLE stateObj.state !== UNAVAILABLE && this._setTemp != null
? this.hass.localize("state.default.unavailable") ? Array.isArray(this._setTemp)
: this._setTemp === undefined || this._setTemp === null
? ""
: Array.isArray(this._setTemp)
? this._stepSize === 1
? svg` ? svg`
${formatNumber(this._setTemp[0], this.hass.locale, { ${this._formatSetTemp(this._setTemp[0])} -
maximumFractionDigits: 0, ${this._formatSetTemp(this._setTemp[1])}
})} - `
${formatNumber(this._setTemp[1], this.hass.locale, { : this._formatSetTemp(this._setTemp)
maximumFractionDigits: 0, : nothing
})}
`
: svg`
${formatNumber(this._setTemp[0], this.hass.locale, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
})} -
${formatNumber(this._setTemp[1], this.hass.locale, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
})}
`
: this._stepSize === 1
? svg`
${formatNumber(this._setTemp, this.hass.locale, {
maximumFractionDigits: 0,
})}
`
: svg`
${formatNumber(this._setTemp, this.hass.locale, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
})}
`
} }
</text> </text>
<text <text
@ -230,7 +205,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
id="set-mode" id="set-mode"
> >
${ ${
stateObj.attributes.hvac_action stateObj.state !== UNAVAILABLE && stateObj.attributes.hvac_action
? computeAttributeValueDisplay( ? computeAttributeValueDisplay(
this.hass.localize, this.hass.localize,
stateObj, stateObj,
@ -248,6 +223,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
) )
} }
${ ${
stateObj.state !== UNAVAILABLE &&
stateObj.attributes.preset_mode && stateObj.attributes.preset_mode &&
stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
? html` ? html`
@ -261,7 +237,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
"preset_mode" "preset_mode"
)} )}
` `
: "" : nothing
} }
</text> </text>
</g> </g>
@ -374,6 +350,17 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
} }
} }
private _formatSetTemp(temp: number) {
return this._stepSize === 1
? formatNumber(temp, this.hass!.locale, {
maximumFractionDigits: 0,
})
: formatNumber(temp, this.hass!.locale, {
minimumFractionDigits: 1,
maximumFractionDigits: 1,
});
}
private _rescale_svg() { private _rescale_svg() {
// Set the viewbox of the SVG containing the set temperature to perfectly // Set the viewbox of the SVG containing the set temperature to perfectly
// fit the text // fit the text

View File

@ -3,11 +3,11 @@ import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers";
import { mdiExclamationThick, mdiHelp } from "@mdi/js"; import { mdiExclamationThick, mdiHelp } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { import {
css,
CSSResultGroup, CSSResultGroup,
html,
LitElement, LitElement,
TemplateResult, TemplateResult,
css,
html,
nothing, nothing,
} from "lit"; } from "lit";
import { import {
@ -37,13 +37,14 @@ import "../../../components/tile/ha-tile-image";
import "../../../components/tile/ha-tile-info"; import "../../../components/tile/ha-tile-info";
import { cameraUrlWithWidthHeight } from "../../../data/camera"; import { cameraUrlWithWidthHeight } from "../../../data/camera";
import { import {
computeCoverPositionStateDisplay,
CoverEntity, CoverEntity,
computeCoverPositionStateDisplay,
} from "../../../data/cover"; } from "../../../data/cover";
import { isUnavailableState } from "../../../data/entity"; import { isUnavailableState } from "../../../data/entity";
import { computeFanSpeedStateDisplay, FanEntity } from "../../../data/fan"; import { FanEntity, computeFanSpeedStateDisplay } from "../../../data/fan";
import { LightEntity } from "../../../data/light"; import type { HumidifierEntity } from "../../../data/humidifier";
import { ActionHandlerEvent } from "../../../data/lovelace"; import type { LightEntity } from "../../../data/light";
import type { ActionHandlerEvent } from "../../../data/lovelace";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive"; import { actionHandler } from "../common/directives/action-handler-directive";
@ -51,15 +52,15 @@ import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action"; import { handleAction } from "../common/handle-action";
import "../components/hui-timestamp-display"; import "../components/hui-timestamp-display";
import { createTileFeatureElement } from "../create-element/create-tile-feature-element"; import { createTileFeatureElement } from "../create-element/create-tile-feature-element";
import { LovelaceTileFeatureConfig } from "../tile-features/types"; import type { LovelaceTileFeatureConfig } from "../tile-features/types";
import { import type {
LovelaceCard, LovelaceCard,
LovelaceCardEditor, LovelaceCardEditor,
LovelaceTileFeature, LovelaceTileFeature,
} from "../types"; } from "../types";
import { HuiErrorCard } from "./hui-error-card"; import type { HuiErrorCard } from "./hui-error-card";
import { computeTileBadge } from "./tile/badges/tile-badge"; import { computeTileBadge } from "./tile/badges/tile-badge";
import { ThermostatCardConfig, TileCardConfig } from "./types"; import type { ThermostatCardConfig, TileCardConfig } from "./types";
const TIMESTAMP_STATE_DOMAINS = ["button", "input_button", "scene"]; const TIMESTAMP_STATE_DOMAINS = ["button", "input_button", "scene"];
@ -224,6 +225,15 @@ export class HuiTileCard extends LitElement implements LovelaceCard {
} }
} }
if (domain === "humidifier" && stateActive(stateObj)) {
const humidity = (stateObj as HumidifierEntity).attributes.humidity;
if (humidity) {
return `${Math.round(humidity)}${blankBeforePercent(
this.hass!.locale
)}%`;
}
}
const stateDisplay = computeStateDisplay( const stateDisplay = computeStateDisplay(
this.hass!.localize, this.hass!.localize,
stateObj, stateObj,