Add option to show current temperature on thermostat card (#19049)

* Fix unit position when no decimal

* Add option to switch between current and target for thermostat card

* Refactor code

* Clean label code

* Rename config name
This commit is contained in:
Paul Bottein 2023-12-20 14:41:22 +01:00 committed by GitHub
parent 2b18ac8d4e
commit af9b64c6f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 209 additions and 111 deletions

View File

@ -120,7 +120,8 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
<p class="title">${name}</p> <p class="title">${name}</p>
<ha-state-control-climate-temperature <ha-state-control-climate-temperature
prevent-interaction-on-scroll prevent-interaction-on-scroll
show-current .showCurrentAsPrimary=${this._config.show_current_as_primary}
show-secondary
.hass=${this.hass} .hass=${this.hass}
.stateObj=${stateObj} .stateObj=${stateObj}
></ha-state-control-climate-temperature> ></ha-state-control-climate-temperature>

View File

@ -456,6 +456,7 @@ export interface ThermostatCardConfig extends LovelaceCardConfig {
entity: string; entity: string;
theme?: string; theme?: string;
name?: string; name?: string;
show_current_as_primary?: boolean;
features?: LovelaceCardFeatureConfig[]; features?: LovelaceCardFeatureConfig[];
} }

View File

@ -6,19 +6,23 @@ import {
array, array,
assert, assert,
assign, assign,
boolean,
object, object,
optional, optional,
string, string,
} from "superstruct"; } from "superstruct";
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event"; import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-form/ha-form"; import "../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../components/ha-form/types"; import type {
HaFormSchema,
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { ThermostatCardConfig } from "../../cards/types";
import { import {
LovelaceCardFeatureConfig, LovelaceCardFeatureConfig,
LovelaceCardFeatureContext, LovelaceCardFeatureContext,
} from "../../card-features/types"; } from "../../card-features/types";
import type { ThermostatCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types"; import type { LovelaceCardEditor } from "../../types";
import "../hui-sub-element-editor"; import "../hui-sub-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct"; import { baseLovelaceCardConfig } from "../structs/base-card-struct";
@ -37,6 +41,7 @@ const cardConfigStruct = assign(
entity: optional(string()), entity: optional(string()),
name: optional(string()), name: optional(string()),
theme: optional(string()), theme: optional(string()),
show_current_as_primary: optional(boolean()),
features: optional(array(any())), features: optional(array(any())),
}) })
); );
@ -51,7 +56,13 @@ const SCHEMA = [
{ name: "theme", selector: { theme: {} } }, { name: "theme", selector: { theme: {} } },
], ],
}, },
] as const; {
name: "show_current_as_primary",
selector: {
boolean: {},
},
},
] as const satisfies readonly HaFormSchema[];
@customElement("hui-thermostat-card-editor") @customElement("hui-thermostat-card-editor")
export class HuiThermostatCardEditor export class HuiThermostatCardEditor
@ -175,9 +186,9 @@ export class HuiThermostatCardEditor
} }
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => { private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
if (schema.name === "entity") { if (schema.name === "show_current_as_primary") {
return this.hass!.localize( return this.hass!.localize(
"ui.panel.lovelace.editor.card.generic.entity" "ui.panel.lovelace.editor.card.thermostat.show_current_as_primary"
); );
} }

View File

@ -1,5 +1,12 @@
import { mdiMinus, mdiPlus, mdiThermometer } from "@mdi/js"; import { mdiMinus, mdiPlus, mdiThermometer, mdiThermostat } from "@mdi/js";
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; import {
CSSResultGroup,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
@ -8,6 +15,8 @@ import { stateActive } from "../../common/entity/state_active";
import { stateColorCss } from "../../common/entity/state_color"; import { stateColorCss } from "../../common/entity/state_color";
import { supportsFeature } from "../../common/entity/supports-feature"; import { supportsFeature } from "../../common/entity/supports-feature";
import { clamp } from "../../common/number/clamp"; import { clamp } from "../../common/number/clamp";
import { formatNumber } from "../../common/number/format_number";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
import { debounce } from "../../common/util/debounce"; import { debounce } from "../../common/util/debounce";
import "../../components/ha-big-number"; import "../../components/ha-big-number";
import "../../components/ha-control-circular-slider"; import "../../components/ha-control-circular-slider";
@ -45,8 +54,11 @@ export class HaStateControlClimateTemperature extends LitElement {
@property({ attribute: false }) public stateObj!: ClimateEntity; @property({ attribute: false }) public stateObj!: ClimateEntity;
@property({ attribute: "show-current", type: Boolean }) @property({ attribute: "show-secondary", type: Boolean })
public showCurrent?: boolean; public showSecondary?: boolean;
@property({ attribute: "use-current-as-primary", type: Boolean })
public showCurrentAsPrimary?: boolean;
@property({ type: Boolean, attribute: "prevent-interaction-on-scroll" }) @property({ type: Boolean, attribute: "prevent-interaction-on-scroll" })
public preventInteractionOnScroll?: boolean; public preventInteractionOnScroll?: boolean;
@ -163,36 +175,13 @@ export class HaStateControlClimateTemperature extends LitElement {
`; `;
} }
if (
(!supportsFeature(
this.stateObj,
ClimateEntityFeature.TARGET_TEMPERATURE
) ||
this._targetTemperature.value === null) &&
(!supportsFeature(
this.stateObj,
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
) ||
this._targetTemperature.low === null ||
this._targetTemperature.high === null)
) {
return html`
<p class="label">${this.hass.formatEntityState(this.stateObj)}</p>
`;
}
const action = this.stateObj.attributes.hvac_action; const action = this.stateObj.attributes.hvac_action;
const actionLabel = this.hass.formatEntityAttributeValue(
this.stateObj,
"hvac_action"
);
return html` return html`
<p class="label"> <p class="label">
${action && action !== "off" && action !== "idle" ${action
? actionLabel ? this.hass.formatEntityAttributeValue(this.stateObj, "hvac_action")
: this.hass.localize("ui.card.climate.target")} : this.hass.formatEntityState(this.stateObj)}
</p> </p>
`; `;
} }
@ -234,52 +223,175 @@ export class HaStateControlClimateTemperature extends LitElement {
`; `;
} }
private _renderTargetTemperature(temperature: number) { private _renderTarget(
temperature: number,
style: "normal" | "big",
hideUnit?: boolean
) {
const digits = this._step.toString().split(".")?.[1]?.length ?? 0; const digits = this._step.toString().split(".")?.[1]?.length ?? 0;
const formatOptions: Intl.NumberFormatOptions = { const formatOptions: Intl.NumberFormatOptions = {
maximumFractionDigits: digits, maximumFractionDigits: digits,
minimumFractionDigits: digits, minimumFractionDigits: digits,
}; };
return html`
<ha-big-number const unit = hideUnit ? "" : this.hass.config.unit_system.temperature;
.value=${temperature}
.unit=${this.hass.config.unit_system.temperature} if (style === "big") {
.hass=${this.hass} return html`
.formatOptions=${formatOptions} <ha-big-number
></ha-big-number> .value=${temperature}
`; .unit=${unit}
.hass=${this.hass}
.formatOptions=${formatOptions}
></ha-big-number>
`;
}
const formatted = formatNumber(
temperature,
this.hass.locale,
formatOptions
);
return html`${formatted}${blankBeforeUnit(unit)}${unit}`;
} }
private _renderCurrentTemperature(temperature?: number) { private _renderCurrent(temperature: number, style: "normal" | "big") {
if (!this.showCurrent || temperature == null) { const formatOptions: Intl.NumberFormatOptions = {
return html`<p class="label">&nbsp;</p>`; maximumFractionDigits: 1,
};
if (style === "big") {
return html`
<ha-big-number
.value=${temperature}
.unit=${this.hass.config.unit_system.temperature}
.hass=${this.hass}
.formatOptions=${formatOptions}
></ha-big-number>
`;
} }
return html` return html`
<p class="label current"> ${this.hass.formatEntityAttributeValue(
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon> this.stateObj,
<span> "current_temperature",
${this.hass.formatEntityAttributeValue( temperature
this.stateObj, )}
"current_temperature",
temperature
)}
</span>
</p>
`; `;
} }
private _renderPrimary() {
const currentTemperature = this.stateObj.attributes.current_temperature;
if (currentTemperature != null && this.showCurrentAsPrimary) {
return this._renderCurrent(currentTemperature, "big");
}
if (this._supportsTargetTemperature && !this.showCurrentAsPrimary) {
return this._renderTarget(this._targetTemperature.value!, "big");
}
if (this._supportsTargetTemperatureRange && !this.showCurrentAsPrimary) {
return html`
<div class="dual">
<button
@click=${this._handleSelectTemp}
.target=${"low"}
class="target-button ${classMap({
selected: this._selectTargetTemperature === "low",
})}"
>
${this._renderTarget(this._targetTemperature.low!, "big")}
</button>
<button
@click=${this._handleSelectTemp}
.target=${"high"}
class="target-button ${classMap({
selected: this._selectTargetTemperature === "high",
})}"
>
${this._renderTarget(this._targetTemperature.high!, "big")}
</button>
</div>
`;
}
return nothing;
}
private _renderSecondary() {
if (!this.showSecondary) {
return html`<p class="label"></p>`;
}
const currentTemperature = this.stateObj.attributes.current_temperature;
if (currentTemperature && !this.showCurrentAsPrimary) {
return html`
<p class="label">
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
${this._renderCurrent(currentTemperature, "normal")}
</p>
`;
}
if (this._supportsTargetTemperature && this.showCurrentAsPrimary) {
return html`
<p class="label">
<ha-svg-icon .path=${mdiThermostat}></ha-svg-icon>
${this._renderTarget(this._targetTemperature.value!, "normal")}
</p>
`;
}
if (this._supportsTargetTemperatureRange && this.showCurrentAsPrimary) {
return html`
<p class="label">
<ha-svg-icon class="target-icon" .path=${mdiThermostat}></ha-svg-icon>
<button
@click=${this._handleSelectTemp}
.target=${"low"}
class="target-button ${classMap({
selected: this._selectTargetTemperature === "low",
})}"
>
${this._renderTarget(this._targetTemperature.low!, "normal", true)}
</button>
<span></span>
<button
@click=${this._handleSelectTemp}
.target=${"high"}
class="target-button ${classMap({
selected: this._selectTargetTemperature === "high",
})}"
>
${this._renderTarget(this._targetTemperature.high!, "normal", true)}
</button>
</p>
`;
}
return html`<p class="label"></p>`;
}
get _supportsTargetTemperature() {
return (
supportsFeature(this.stateObj, ClimateEntityFeature.TARGET_TEMPERATURE) &&
this._targetTemperature.value != null
);
}
get _supportsTargetTemperatureRange() {
return (
supportsFeature(
this.stateObj,
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
) &&
this._targetTemperature.low != null &&
this._targetTemperature.high != null
);
}
protected render() { protected render() {
const supportsTargetTemperature = supportsFeature(
this.stateObj,
ClimateEntityFeature.TARGET_TEMPERATURE
);
const supportsTargetTemperatureRange = supportsFeature(
this.stateObj,
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
);
const mode = this.stateObj.state; const mode = this.stateObj.state;
const action = this.stateObj.attributes.hvac_action; const action = this.stateObj.attributes.hvac_action;
const active = stateActive(this.stateObj); const active = stateActive(this.stateObj);
@ -301,8 +413,7 @@ export class HaStateControlClimateTemperature extends LitElement {
: ""; : "";
if ( if (
supportsTargetTemperature && this._supportsTargetTemperature &&
this._targetTemperature.value != null &&
this.stateObj.state !== UNAVAILABLE this.stateObj.state !== UNAVAILABLE
) { ) {
const heatCoolModes = this.stateObj.attributes.hvac_modes.filter((m) => const heatCoolModes = this.stateObj.attributes.hvac_modes.filter((m) =>
@ -337,11 +448,7 @@ export class HaStateControlClimateTemperature extends LitElement {
> >
</ha-control-circular-slider> </ha-control-circular-slider>
<div class="info"> <div class="info">
${this._renderLabel()} ${this._renderLabel()}${this._renderPrimary()}${this._renderSecondary()}
${this._renderTargetTemperature(this._targetTemperature.value)}
${this._renderCurrentTemperature(
this.stateObj.attributes.current_temperature
)}
</div> </div>
${this._renderTemperatureButtons("value")} ${this._renderTemperatureButtons("value")}
</div> </div>
@ -349,9 +456,7 @@ export class HaStateControlClimateTemperature extends LitElement {
} }
if ( if (
supportsTargetTemperatureRange && this._supportsTargetTemperatureRange &&
this._targetTemperature.low != null &&
this._targetTemperature.high != null &&
this.stateObj.state !== UNAVAILABLE this.stateObj.state !== UNAVAILABLE
) { ) {
return html` return html`
@ -380,30 +485,7 @@ export class HaStateControlClimateTemperature extends LitElement {
> >
</ha-control-circular-slider> </ha-control-circular-slider>
<div class="info"> <div class="info">
${this._renderLabel()} ${this._renderLabel()}${this._renderPrimary()}${this._renderSecondary()}
<div class="dual">
<button
@click=${this._handleSelectTemp}
.target=${"low"}
class=${classMap({
selected: this._selectTargetTemperature === "low",
})}
>
${this._renderTargetTemperature(this._targetTemperature.low)}
</button>
<button
@click=${this._handleSelectTemp}
.target=${"high"}
class=${classMap({
selected: this._selectTargetTemperature === "high",
})}
>
${this._renderTargetTemperature(this._targetTemperature.high)}
</button>
</div>
${this._renderCurrentTemperature(
this.stateObj.attributes.current_temperature
)}
</div> </div>
${this._renderTemperatureButtons(this._selectTargetTemperature, true)} ${this._renderTemperatureButtons(this._selectTargetTemperature, true)}
</div> </div>
@ -429,10 +511,7 @@ export class HaStateControlClimateTemperature extends LitElement {
> >
</ha-control-circular-slider> </ha-control-circular-slider>
<div class="info"> <div class="info">
${this._renderLabel()} ${this._renderLabel()} ${this._renderSecondary()}
${this._renderCurrentTemperature(
this.stateObj.attributes.current_temperature
)}
</div> </div>
</div> </div>
`; `;
@ -448,24 +527,26 @@ export class HaStateControlClimateTemperature extends LitElement {
flex-direction: row; flex-direction: row;
gap: 24px; gap: 24px;
} }
.dual button { .target-button {
outline: none; outline: none;
background: none; background: none;
color: inherit; color: inherit;
font-family: inherit; font-family: inherit;
font-size: inherit;
font-weight: inherit;
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent;
border: none; border: none;
opacity: 0.5; opacity: 0.7;
padding: 0; padding: 0;
transition: transition:
opacity 180ms ease-in-out, opacity 180ms ease-in-out,
transform 180ms ease-in-out; transform 180ms ease-in-out;
cursor: pointer; cursor: pointer;
} }
.dual button:focus-visible { .target-button:focus-visible {
transform: scale(1.1); transform: scale(1.1);
} }
.dual button.selected { .target-button.selected {
opacity: 1; opacity: 1;
} }
.container.md .dual { .container.md .dual {
@ -475,6 +556,9 @@ export class HaStateControlClimateTemperature extends LitElement {
.container.xs .dual { .container.xs .dual {
gap: 8px; gap: 8px;
} }
.container.sm .target-icon {
display: none;
}
ha-control-circular-slider { ha-control-circular-slider {
--control-circular-slider-low-color: var( --control-circular-slider-low-color: var(
--low-color, --low-color,

View File

@ -5235,7 +5235,8 @@
}, },
"thermostat": { "thermostat": {
"name": "Thermostat", "name": "Thermostat",
"description": "The Thermostat card gives control of your climate entity. Allowing you to change the temperature and mode of the entity." "description": "The Thermostat card gives control of your climate entity. Allowing you to change the temperature and mode of the entity.",
"show_current_as_primary": "Show current temperature as primary information"
}, },
"tile": { "tile": {
"name": "Tile", "name": "Tile",