mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add new design to thermostat card (#18709)
* Add new design for thermostat card * Add feature to thermostat card * Fix margin * Add current * use big number component * Add current * Fix translations * Add theme and name options * Reduce margin on small card * Fix types * Add hvac mode to default dashboard * Don't put feature full size * Full width for features * Improve design on small screen
This commit is contained in:
parent
1526209f82
commit
c787c920fc
114
src/components/ha-big-number.ts
Normal file
114
src/components/ha-big-number.ts
Normal file
@ -0,0 +1,114 @@
|
||||
import { CSSResultGroup, LitElement, css, html } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { blankBeforeUnit } from "../common/translations/blank_before_unit";
|
||||
import { HomeAssistant } from "../types";
|
||||
|
||||
@customElement("ha-big-number")
|
||||
export class HaBigNumber extends LitElement {
|
||||
@property() public value!: number;
|
||||
|
||||
@property() public unit?: string;
|
||||
|
||||
@property({ attribute: "unit-position" })
|
||||
public unitPosition: "top" | "bottom" = "top";
|
||||
|
||||
@property({ attribute: false })
|
||||
public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
public formatOptions: Intl.NumberFormatOptions = {};
|
||||
|
||||
protected render() {
|
||||
const formatted = formatNumber(
|
||||
this.value,
|
||||
this.hass?.locale,
|
||||
this.formatOptions
|
||||
);
|
||||
const [integer] = formatted.includes(".")
|
||||
? formatted.split(".")
|
||||
: formatted.split(",");
|
||||
|
||||
const temperatureDecimal = formatted.replace(integer, "");
|
||||
|
||||
const formattedValue = `${this.value}${
|
||||
this.unit ? `${blankBeforeUnit(this.unit)}${this.unit}` : ""
|
||||
}`;
|
||||
|
||||
const unitBottom = this.unitPosition === "bottom";
|
||||
|
||||
return html`
|
||||
<p class="value">
|
||||
<span aria-hidden="true" class="displayed-value">
|
||||
<span>${integer}</span>
|
||||
<span class="addon ${classMap({ bottom: unitBottom })}">
|
||||
<span class="decimal">${temperatureDecimal}</span>
|
||||
<span class="unit">${this.unit}</span>
|
||||
</span>
|
||||
</span>
|
||||
<span class="visually-hidden">${formattedValue}</span>
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
css`
|
||||
:host {
|
||||
font-size: 57px;
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.25px;
|
||||
}
|
||||
.value {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
direction: ltr;
|
||||
}
|
||||
.displayed-value {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.addon {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.addon.bottom {
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
.addon.bottom .unit {
|
||||
margin-bottom: 4px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.value .decimal {
|
||||
font-size: 0.42em;
|
||||
line-height: 1.33;
|
||||
}
|
||||
.value .unit {
|
||||
font-size: 0.33em;
|
||||
line-height: 1.26;
|
||||
}
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-big-number": HaBigNumber;
|
||||
}
|
||||
}
|
@ -618,9 +618,11 @@ export class HaControlCircularSlider extends LitElement {
|
||||
--control-circular-slider-high-color: var(
|
||||
--control-circular-slider-color
|
||||
);
|
||||
width: 320px;
|
||||
display: block;
|
||||
}
|
||||
svg {
|
||||
width: 320px;
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
#slider {
|
||||
|
@ -44,7 +44,7 @@ export class HaControlNumberButton extends LitElement {
|
||||
|
||||
@property() public unit?: string;
|
||||
|
||||
@property({ attribute: "false" })
|
||||
@property({ attribute: false })
|
||||
public formatOptions: Intl.NumberFormatOptions = {};
|
||||
|
||||
@query("#input") _input!: HTMLDivElement;
|
||||
|
@ -262,7 +262,7 @@ export class HaControlSelect extends LitElement {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||
import { mdiMinus, mdiPlus, mdiWaterPercent } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { stateActive } from "../../../../common/entity/state_active";
|
||||
@ -7,6 +8,7 @@ import { domainStateColorProperties } from "../../../../common/entity/state_colo
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-big-number";
|
||||
import "../../../../components/ha-control-circular-slider";
|
||||
import "../../../../components/ha-outlined-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
@ -22,6 +24,9 @@ export class HaMoreInfoClimateHumidity extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj!: ClimateEntity;
|
||||
|
||||
@property({ attribute: "show-current", type: Boolean })
|
||||
public showCurrent?: boolean;
|
||||
|
||||
@state() private _targetHumidity?: number;
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
@ -87,9 +92,7 @@ export class HaMoreInfoClimateHumidity extends LitElement {
|
||||
|
||||
return html`
|
||||
<p class="label">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.climate.humidity_target"
|
||||
)}
|
||||
${this.hass.localize("ui.card.climate.humidity_target")}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
@ -114,20 +117,37 @@ export class HaMoreInfoClimateHumidity extends LitElement {
|
||||
}
|
||||
|
||||
private _renderTarget(humidity: number) {
|
||||
const rounded = Math.round(humidity);
|
||||
const formatted = this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"humidity",
|
||||
rounded
|
||||
);
|
||||
const formatOptions = {
|
||||
maximumFractionDigits: 0,
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="target">
|
||||
<p class="value" aria-hidden="true">
|
||||
${rounded}<span class="unit">%</span>
|
||||
</p>
|
||||
<p class="visually-hidden">${formatted}</p>
|
||||
</div>
|
||||
<ha-big-number
|
||||
.value=${humidity}
|
||||
unit="%"
|
||||
unit-position="bottom"
|
||||
.hass=${this.hass}
|
||||
.formatOptions=${formatOptions}
|
||||
></ha-big-number>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderCurrentHumidity(humidity?: number) {
|
||||
if (!this.showCurrent || humidity == null) {
|
||||
return html`<p class="label"> </p>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<p class="label">
|
||||
<ha-svg-icon .path=${mdiWaterPercent}></ha-svg-icon>
|
||||
<span>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity",
|
||||
humidity
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -174,10 +194,10 @@ export class HaMoreInfoClimateHumidity extends LitElement {
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="label-container">${this._renderLabel()}</div>
|
||||
<div class="target-container">
|
||||
${this._renderTarget(targetHumidity)}
|
||||
</div>
|
||||
${this._renderLabel()} ${this._renderTarget(targetHumidity)}
|
||||
${this._renderCurrentHumidity(
|
||||
this.stateObj.attributes.current_humidity
|
||||
)}
|
||||
</div>
|
||||
${this._renderButtons()}
|
||||
</div>
|
||||
@ -195,32 +215,17 @@ export class HaMoreInfoClimateHumidity extends LitElement {
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="label-container">${this._renderLabel()}</div>
|
||||
${this._renderLabel()}
|
||||
${this._renderCurrentHumidity(
|
||||
this.stateObj.attributes.current_humidity
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
moreInfoControlCircularSliderStyle,
|
||||
css`
|
||||
/* Elements */
|
||||
.target-container {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.target .value {
|
||||
font-size: 58px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.25px;
|
||||
}
|
||||
.target .value .unit {
|
||||
font-size: 0.4em;
|
||||
line-height: 1;
|
||||
margin-left: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return moreInfoControlCircularSliderStyle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,5 @@
|
||||
import { mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { mdiMinus, mdiPlus, mdiThermometer } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
@ -15,8 +8,8 @@ import { stateActive } from "../../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-big-number";
|
||||
import "../../../../components/ha-control-circular-slider";
|
||||
import type { ControlCircularSliderMode } from "../../../../components/ha-control-circular-slider";
|
||||
import "../../../../components/ha-outlined-icon-button";
|
||||
@ -49,6 +42,9 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj!: ClimateEntity;
|
||||
|
||||
@property({ attribute: "show-current", type: Boolean })
|
||||
public showCurrent?: boolean;
|
||||
|
||||
@state() private _targetTemperature: Partial<Record<Target, number>> = {};
|
||||
|
||||
@state() private _selectTargetTemperature: Target = "low";
|
||||
@ -183,14 +179,9 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
|
||||
return html`
|
||||
<p class="label">
|
||||
${action && ["preheating", "heating", "cooling"].includes(action)
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.more_info_control.climate.target_label",
|
||||
{ action: actionLabel }
|
||||
)
|
||||
: action && action !== "off" && action !== "idle"
|
||||
? actionLabel
|
||||
: this.hass.localize("ui.dialogs.more_info_control.climate.target")}
|
||||
${action && action !== "off" && action !== "idle"
|
||||
? actionLabel
|
||||
: this.hass.localize("ui.card.climate.target")}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
@ -234,30 +225,34 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
|
||||
private _renderTargetTemperature(temperature: number) {
|
||||
const digits = this._step.toString().split(".")?.[1]?.length ?? 0;
|
||||
const formatted = formatNumber(temperature, this.hass.locale, {
|
||||
const formatOptions: Intl.NumberFormatOptions = {
|
||||
maximumFractionDigits: digits,
|
||||
minimumFractionDigits: digits,
|
||||
});
|
||||
const [temperatureInteger] = formatted.includes(".")
|
||||
? formatted.split(".")
|
||||
: formatted.split(",");
|
||||
};
|
||||
return html`
|
||||
<ha-big-number
|
||||
.value=${temperature}
|
||||
.unit=${this.hass.config.unit_system.temperature}
|
||||
.hass=${this.hass}
|
||||
.formatOptions=${formatOptions}
|
||||
></ha-big-number>
|
||||
`;
|
||||
}
|
||||
|
||||
const temperatureDecimal = formatted.replace(temperatureInteger, "");
|
||||
private _renderCurrentTemperature(temperature?: number) {
|
||||
if (!this.showCurrent || temperature == null) {
|
||||
return html`<p class="label"> </p>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<p class="temperature">
|
||||
<span aria-hidden="true">
|
||||
${temperatureInteger}
|
||||
${digits !== 0
|
||||
? html`<span class="decimal">${temperatureDecimal}</span>`
|
||||
: nothing}
|
||||
<span class="unit">
|
||||
${this.hass.config.unit_system.temperature}
|
||||
</span>
|
||||
</span>
|
||||
<span class="visually-hidden">
|
||||
${this.stateObj.attributes.temperature}
|
||||
${this.hass.config.unit_system.temperature}
|
||||
<p class="label current">
|
||||
<ha-svg-icon .path=${mdiThermometer}></ha-svg-icon>
|
||||
<span>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_temperature",
|
||||
temperature
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
`;
|
||||
@ -326,10 +321,11 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="label-container">${this._renderLabel()}</div>
|
||||
<div class="temperature-container">
|
||||
${this._renderTargetTemperature(this._targetTemperature.value)}
|
||||
</div>
|
||||
${this._renderLabel()}
|
||||
${this._renderTargetTemperature(this._targetTemperature.value)}
|
||||
${this._renderCurrentTemperature(
|
||||
this.stateObj.attributes.current_temperature
|
||||
)}
|
||||
</div>
|
||||
${this._renderTemperatureButtons("value")}
|
||||
</div>
|
||||
@ -367,8 +363,8 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="label-container">${this._renderLabel()}</div>
|
||||
<div class="temperature-container dual">
|
||||
${this._renderLabel()}
|
||||
<div class="dual">
|
||||
<button
|
||||
@click=${this._handleSelectTemp}
|
||||
.target=${"low"}
|
||||
@ -388,6 +384,9 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
${this._renderTargetTemperature(this._targetTemperature.high)}
|
||||
</button>
|
||||
</div>
|
||||
${this._renderCurrentTemperature(
|
||||
this.stateObj.attributes.current_temperature
|
||||
)}
|
||||
</div>
|
||||
${this._renderTemperatureButtons(this._selectTargetTemperature, true)}
|
||||
</div>
|
||||
@ -412,7 +411,10 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="label-container">${this._renderLabel()}</div>
|
||||
${this._renderLabel()}
|
||||
${this._renderCurrentTemperature(
|
||||
this.stateObj.attributes.current_temperature
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@ -422,45 +424,12 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
return [
|
||||
moreInfoControlCircularSliderStyle,
|
||||
css`
|
||||
/* Elements */
|
||||
.temperature-container {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.temperature {
|
||||
display: inline-flex;
|
||||
font-size: 58px;
|
||||
line-height: 64px;
|
||||
letter-spacing: -0.25px;
|
||||
margin: 0;
|
||||
direction: ltr;
|
||||
}
|
||||
.temperature span {
|
||||
display: inline-flex;
|
||||
}
|
||||
.temperature .decimal {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
align-self: flex-end;
|
||||
width: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.temperature .unit {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
align-self: flex-start;
|
||||
width: 20px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.decimal + .unit {
|
||||
margin-left: -20px;
|
||||
}
|
||||
/* Dual target */
|
||||
.dual {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.dual button {
|
||||
outline: none;
|
||||
background: none;
|
||||
@ -481,7 +450,16 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
.dual button.selected {
|
||||
opacity: 1;
|
||||
}
|
||||
/* Slider */
|
||||
@container container (max-width: 250px) {
|
||||
.dual {
|
||||
gap: 16px;
|
||||
}
|
||||
}
|
||||
@container container (max-width: 190px) {
|
||||
.dual {
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
ha-control-circular-slider {
|
||||
--control-circular-slider-low-color: var(
|
||||
--low-color,
|
||||
|
@ -2,8 +2,13 @@ import { css } from "lit";
|
||||
|
||||
export const moreInfoControlCircularSliderStyle = css`
|
||||
/* Layout elements */
|
||||
:host {
|
||||
width: 320px;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
container-type: inline-size;
|
||||
container-name: container;
|
||||
}
|
||||
.info {
|
||||
position: absolute;
|
||||
@ -17,24 +22,17 @@ export const moreInfoControlCircularSliderStyle = css`
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
line-height: 1.5;
|
||||
letter-spacing: 0.1px;
|
||||
gap: 8px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
.info * {
|
||||
margin: 0;
|
||||
pointer-events: auto;
|
||||
}
|
||||
/* Info elements */
|
||||
.label-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 200px;
|
||||
height: 48px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.label {
|
||||
width: 60%;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: var(--action-color, inherit);
|
||||
@ -43,6 +41,12 @@ export const moreInfoControlCircularSliderStyle = css`
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.label span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.label ha-svg-icon {
|
||||
bottom: 5%;
|
||||
}
|
||||
.label.disabled {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
@ -53,30 +57,52 @@ export const moreInfoControlCircularSliderStyle = css`
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
width: 120px;
|
||||
gap: 24px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.buttons ha-outlined-icon-button {
|
||||
--md-outlined-icon-button-container-width: 48px;
|
||||
--md-outlined-icon-button-container-height: 48px;
|
||||
--md-outlined-icon-button-icon-size: 24px;
|
||||
}
|
||||
/* Accessibility */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
height: 1px;
|
||||
width: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
|
||||
@container container (max-width: 250px) {
|
||||
ha-big-number {
|
||||
font-size: 44px;
|
||||
}
|
||||
.buttons {
|
||||
gap: 16px;
|
||||
}
|
||||
.info {
|
||||
margin-top: 12px;
|
||||
gap: 6px;
|
||||
}
|
||||
.buttons {
|
||||
display: none;
|
||||
}
|
||||
ha-control-circular-slider {
|
||||
margin-bottom: -16px;
|
||||
}
|
||||
}
|
||||
@container container (max-width: 190px) {
|
||||
ha-big-number {
|
||||
font-size: 32px;
|
||||
}
|
||||
.info {
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
gap: 2px;
|
||||
--mdc-icon-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Slider */
|
||||
ha-control-circular-slider {
|
||||
width: 100%;
|
||||
--control-circular-slider-color: var(--state-color, var(--disabled-color));
|
||||
}
|
||||
ha-control-circular-slider::after {
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { stateActive } from "../../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { blankBeforePercent } from "../../../../common/translations/blank_before_percent";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-big-number";
|
||||
import "../../../../components/ha-control-circular-slider";
|
||||
import "../../../../components/ha-outlined-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
@ -26,6 +25,9 @@ export class HaMoreInfoHumidifierHumidity extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HumidifierEntity;
|
||||
|
||||
@property({ attribute: "show-current", type: Boolean })
|
||||
public showCurrent?: boolean = false;
|
||||
|
||||
@state() private _targetHumidity?: number;
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
@ -99,15 +101,31 @@ export class HaMoreInfoHumidifierHumidity extends LitElement {
|
||||
return html`
|
||||
<p class="label">
|
||||
${action && ["drying", "humidifying"].includes(action)
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.more_info_control.humidifier.target_label",
|
||||
{ action: actionLabel }
|
||||
)
|
||||
? this.hass.localize("ui.card.humidifier.target_label", {
|
||||
action: actionLabel,
|
||||
})
|
||||
: action && action !== "off" && action !== "idle"
|
||||
? actionLabel
|
||||
: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.humidifier.target"
|
||||
)}
|
||||
: this.hass.localize("ui.card.humidifier.target")}
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderCurrentHumidity(humidity?: number) {
|
||||
if (!this.showCurrent || humidity == null) {
|
||||
return html`<p class="label"> </p>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<p class="label">
|
||||
${this.hass.localize("ui.card.humidifier.currently")}
|
||||
<span>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_humidity",
|
||||
humidity
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
`;
|
||||
}
|
||||
@ -132,19 +150,18 @@ export class HaMoreInfoHumidifierHumidity extends LitElement {
|
||||
}
|
||||
|
||||
private _renderTarget(humidity: number) {
|
||||
const formatted = formatNumber(humidity, this.hass.locale, {
|
||||
const formatOptions = {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
};
|
||||
|
||||
return html`
|
||||
<div class="target">
|
||||
<p class="value" aria-hidden="true">
|
||||
${formatted}<span class="unit">%</span>
|
||||
</p>
|
||||
<p class="visually-hidden">
|
||||
${formatted}${blankBeforePercent(this.hass.locale)}%
|
||||
</p>
|
||||
</div>
|
||||
<ha-big-number
|
||||
.value=${humidity}
|
||||
unit="%"
|
||||
unit-position="bottom"
|
||||
.hass=${this.hass}
|
||||
.formatOptions=${formatOptions}
|
||||
></ha-big-number>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -191,10 +208,10 @@ export class HaMoreInfoHumidifierHumidity extends LitElement {
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="label-container">${this._renderLabel()}</div>
|
||||
<div class="target-container">
|
||||
${this._renderTarget(targetHumidity)}
|
||||
</div>
|
||||
${this._renderLabel()} ${this._renderTarget(targetHumidity)}
|
||||
${this._renderCurrentHumidity(
|
||||
this.stateObj.attributes.current_humidity
|
||||
)}
|
||||
</div>
|
||||
${this._renderButtons()}
|
||||
</div>
|
||||
@ -217,32 +234,17 @@ export class HaMoreInfoHumidifierHumidity extends LitElement {
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="label-container">${this._renderLabel()}</div>
|
||||
${this._renderLabel()}
|
||||
${this._renderCurrentHumidity(
|
||||
this.stateObj.attributes.current_humidity
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
moreInfoControlCircularSliderStyle,
|
||||
css`
|
||||
/* Elements */
|
||||
.target-container {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.target .value {
|
||||
font-size: 58px;
|
||||
line-height: 1;
|
||||
letter-spacing: -0.25px;
|
||||
}
|
||||
.target .value .unit {
|
||||
font-size: 0.4em;
|
||||
line-height: 1;
|
||||
margin-left: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return moreInfoControlCircularSliderStyle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,5 @@
|
||||
import { mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
} from "lit";
|
||||
import { CSSResultGroup, LitElement, PropertyValues, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { UNIT_F } from "../../../../common/const";
|
||||
@ -14,8 +7,8 @@ import { stateActive } from "../../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import { debounce } from "../../../../common/util/debounce";
|
||||
import "../../../../components/ha-big-number";
|
||||
import "../../../../components/ha-control-circular-slider";
|
||||
import "../../../../components/ha-outlined-icon-button";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
@ -33,6 +26,9 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj!: WaterHeaterEntity;
|
||||
|
||||
@property({ attribute: "show-current", type: Boolean })
|
||||
public showCurrent?: boolean;
|
||||
|
||||
@state() private _targetTemperature?: number;
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
@ -111,11 +107,7 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
|
||||
}
|
||||
|
||||
return html`
|
||||
<p class="label">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.water_heater.target"
|
||||
)}
|
||||
</p>
|
||||
<p class="label">${this.hass.localize("ui.card.water_heater.target")}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -140,30 +132,34 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
|
||||
|
||||
private _renderTargetTemperature(temperature: number) {
|
||||
const digits = this._step.toString().split(".")?.[1]?.length ?? 0;
|
||||
const formatted = formatNumber(temperature, this.hass.locale, {
|
||||
const formatOptions: Intl.NumberFormatOptions = {
|
||||
maximumFractionDigits: digits,
|
||||
minimumFractionDigits: digits,
|
||||
});
|
||||
const [temperatureInteger] = formatted.includes(".")
|
||||
? formatted.split(".")
|
||||
: formatted.split(",");
|
||||
};
|
||||
return html`
|
||||
<ha-big-number
|
||||
.value=${temperature}
|
||||
.unit=${this.hass.config.unit_system.temperature}
|
||||
.hass=${this.hass}
|
||||
.formatOptions=${formatOptions}
|
||||
></ha-big-number>
|
||||
`;
|
||||
}
|
||||
|
||||
const temperatureDecimal = formatted.replace(temperatureInteger, "");
|
||||
private _renderCurrentTemperature(temperature?: number) {
|
||||
if (!this.showCurrent || temperature == null) {
|
||||
return html`<p class="label"> </p>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<p class="temperature">
|
||||
<span aria-hidden="true">
|
||||
${temperatureInteger}
|
||||
${digits !== 0
|
||||
? html`<span class="decimal">${temperatureDecimal}</span>`
|
||||
: nothing}
|
||||
<span class="unit">
|
||||
${this.hass.config.unit_system.temperature}
|
||||
</span>
|
||||
</span>
|
||||
<span class="visually-hidden">
|
||||
${this.stateObj.attributes.temperature}
|
||||
${this.hass.config.unit_system.temperature}
|
||||
<p class="label">
|
||||
${this.hass.localize("ui.card.water_heater.currently")}
|
||||
<span>
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
this.stateObj,
|
||||
"current_temperature",
|
||||
temperature
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
`;
|
||||
@ -202,10 +198,11 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="label-container">${this._renderLabel()}</div>
|
||||
<div class="temperature-container">
|
||||
${this._renderTargetTemperature(this._targetTemperature)}
|
||||
</div>
|
||||
${this._renderLabel()}
|
||||
${this._renderTargetTemperature(this._targetTemperature)}
|
||||
${this._renderCurrentTemperature(
|
||||
this.stateObj.attributes.current_temperature
|
||||
)}
|
||||
</div>
|
||||
${this._renderButtons()}
|
||||
</div>
|
||||
@ -230,49 +227,17 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
|
||||
>
|
||||
</ha-control-circular-slider>
|
||||
<div class="info">
|
||||
<div class="label-container">${this._renderLabel()}</div>
|
||||
${this._renderLabel()}
|
||||
${this._renderCurrentTemperature(
|
||||
this.stateObj.attributes.current_temperature
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
moreInfoControlCircularSliderStyle,
|
||||
css`
|
||||
/* Elements */
|
||||
.temperature-container {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.temperature {
|
||||
display: inline-flex;
|
||||
font-size: 58px;
|
||||
line-height: 64px;
|
||||
letter-spacing: -0.25px;
|
||||
margin: 0;
|
||||
}
|
||||
.temperature span {
|
||||
display: inline-flex;
|
||||
}
|
||||
.temperature .decimal {
|
||||
font-size: 24px;
|
||||
line-height: 32px;
|
||||
align-self: flex-end;
|
||||
width: 20px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.temperature .unit {
|
||||
font-size: 20px;
|
||||
line-height: 24px;
|
||||
align-self: flex-start;
|
||||
width: 20px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.decimal + .unit {
|
||||
margin-left: -20px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return moreInfoControlCircularSliderStyle;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,5 @@
|
||||
import {
|
||||
mdiAutorenew,
|
||||
mdiCalendarSync,
|
||||
mdiDotsVertical,
|
||||
mdiFan,
|
||||
mdiFire,
|
||||
mdiPower,
|
||||
mdiSnowflake,
|
||||
mdiWaterPercent,
|
||||
} from "@mdi/js";
|
||||
import { mdiDotsVertical } from "@mdi/js";
|
||||
import "@thomasloven/round-slider";
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
@ -17,45 +7,24 @@ import {
|
||||
css,
|
||||
html,
|
||||
nothing,
|
||||
svg,
|
||||
} from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-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 { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { formatNumber } from "../../../common/number/format_number";
|
||||
import "../../../components/ha-card";
|
||||
import type { HaCard } from "../../../components/ha-card";
|
||||
import "../../../components/ha-icon-button";
|
||||
import {
|
||||
CLIMATE_PRESET_NONE,
|
||||
ClimateEntity,
|
||||
HvacMode,
|
||||
compareClimateHvacModes,
|
||||
} from "../../../data/climate";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import { ClimateEntity } from "../../../data/climate";
|
||||
import "../../../dialogs/more-info/components/climate/ha-more-info-climate-temperature";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { findEntities } from "../common/find-entities";
|
||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||
import { createEntityNotFoundWarning } from "../components/hui-warning";
|
||||
import "../tile-features/hui-tile-features";
|
||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||
import { ThermostatCardConfig } from "./types";
|
||||
|
||||
// TODO: Need to align these icon to more info icons
|
||||
const modeIcons: { [mode in HvacMode]: string } = {
|
||||
auto: mdiCalendarSync,
|
||||
heat_cool: mdiAutorenew,
|
||||
heat: mdiFire,
|
||||
cool: mdiSnowflake,
|
||||
off: mdiPower,
|
||||
fan_only: mdiFan,
|
||||
dry: mdiWaterPercent,
|
||||
};
|
||||
|
||||
@customElement("hui-thermostat-card")
|
||||
export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||
@ -85,12 +54,6 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
|
||||
@state() private _config?: ThermostatCardConfig;
|
||||
|
||||
@state() private _setTemp?: number | number[];
|
||||
|
||||
@query("ha-card") private _card?: HaCard;
|
||||
|
||||
@state() private resyncSetpoint = false;
|
||||
|
||||
public getCardSize(): number {
|
||||
return 7;
|
||||
}
|
||||
@ -103,173 +66,10 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
<hui-warning>
|
||||
${createEntityNotFoundWarning(this.hass, this._config.entity)}
|
||||
</hui-warning>
|
||||
`;
|
||||
}
|
||||
|
||||
const mode = stateObj.state in modeIcons ? stateObj.state : "unknown-mode";
|
||||
const name =
|
||||
this._config!.name ||
|
||||
computeStateName(this.hass!.states[this._config!.entity]);
|
||||
const targetTemp = this.resyncSetpoint
|
||||
? // If the user set position in the slider is out of sync with the entity
|
||||
// value, then rerendering the slider with same $value a second time
|
||||
// does not move the slider. Need to set it to a different dummy value
|
||||
// for one update cycle to force it to rerender to the desired value.
|
||||
stateObj.attributes.min_temp - 1
|
||||
: stateObj.attributes.temperature !== null &&
|
||||
Number.isFinite(Number(stateObj.attributes.temperature))
|
||||
? stateObj.attributes.temperature
|
||||
: stateObj.attributes.min_temp;
|
||||
|
||||
const targetLow = this.resyncSetpoint
|
||||
? stateObj.attributes.min_temp - 1
|
||||
: stateObj.attributes.target_temp_low;
|
||||
const targetHigh = this.resyncSetpoint
|
||||
? stateObj.attributes.min_temp - 1
|
||||
: stateObj.attributes.target_temp_high;
|
||||
|
||||
const slider =
|
||||
stateObj.state === UNAVAILABLE
|
||||
? html` <round-slider disabled="true"></round-slider> `
|
||||
: html`
|
||||
<round-slider
|
||||
.value=${targetTemp}
|
||||
.low=${targetLow}
|
||||
.high=${targetHigh}
|
||||
.min=${stateObj.attributes.min_temp}
|
||||
.max=${stateObj.attributes.max_temp}
|
||||
.step=${this._stepSize}
|
||||
@value-changing=${this._dragEvent}
|
||||
@value-changed=${this._setTemperature}
|
||||
></round-slider>
|
||||
`;
|
||||
|
||||
const currentTemperature = svg`
|
||||
<svg viewBox="0 0 40 20">
|
||||
<text
|
||||
x="50%"
|
||||
dx="1"
|
||||
y="60%"
|
||||
text-anchor="middle"
|
||||
style="font-size: 13px;"
|
||||
>
|
||||
${
|
||||
stateObj.state !== UNAVAILABLE &&
|
||||
stateObj.attributes.current_temperature != null &&
|
||||
!isNaN(stateObj.attributes.current_temperature)
|
||||
? svg`
|
||||
${formatNumber(
|
||||
stateObj.attributes.current_temperature,
|
||||
this.hass.locale
|
||||
)}
|
||||
<tspan dx="-3" dy="-6.5" style="font-size: 4px;">
|
||||
${this.hass.config.unit_system.temperature}
|
||||
</tspan>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</text>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
const setValues = svg`
|
||||
<svg id="set-values">
|
||||
<g>
|
||||
<text text-anchor="middle" class="set-value">
|
||||
${
|
||||
stateObj.state !== UNAVAILABLE && this._setTemp != null
|
||||
? Array.isArray(this._setTemp)
|
||||
? svg`
|
||||
${this._formatSetTemp(this._setTemp[0])} -
|
||||
${this._formatSetTemp(this._setTemp[1])}
|
||||
`
|
||||
: this._formatSetTemp(this._setTemp)
|
||||
: nothing
|
||||
}
|
||||
</text>
|
||||
<text
|
||||
dy="22"
|
||||
text-anchor="middle"
|
||||
id="set-mode"
|
||||
>
|
||||
${
|
||||
stateObj.state !== UNAVAILABLE && stateObj.attributes.hvac_action
|
||||
? this.hass.formatEntityAttributeValue(stateObj, "hvac_action")
|
||||
: this.hass.formatEntityState(stateObj)
|
||||
}
|
||||
${
|
||||
stateObj.state !== UNAVAILABLE &&
|
||||
stateObj.attributes.preset_mode &&
|
||||
stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE
|
||||
? html`
|
||||
-
|
||||
${this.hass.formatEntityAttributeValue(
|
||||
stateObj,
|
||||
"preset_mode"
|
||||
)}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</text>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
style=${styleMap({
|
||||
"--mode-color": stateColorCss(stateObj),
|
||||
})}
|
||||
>
|
||||
<ha-icon-button
|
||||
class="more-info"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.show_more_info"
|
||||
)}
|
||||
.path=${mdiDotsVertical}
|
||||
@click=${this._handleMoreInfo}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
|
||||
<div class="content">
|
||||
<div id="controls">
|
||||
<div id="slider">
|
||||
${slider}
|
||||
<div id="slider-center">
|
||||
<div id="temperature">${currentTemperature} ${setValues}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="info" .title=${name}>
|
||||
<div id="modes">
|
||||
${(stateObj.attributes.hvac_modes || [])
|
||||
.concat()
|
||||
.sort(compareClimateHvacModes)
|
||||
.map((modeItem) => this._renderIcon(modeItem, mode))}
|
||||
</div>
|
||||
${name}
|
||||
</div>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
return (
|
||||
hasConfigOrEntityChanged(this, changedProps) ||
|
||||
changedProps.has("resyncSetpoint")
|
||||
);
|
||||
private _handleMoreInfo() {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: this._config!.entity,
|
||||
});
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
@ -296,223 +96,56 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
) {
|
||||
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) {
|
||||
if (
|
||||
!this.hass ||
|
||||
!this._config ||
|
||||
!(changedProps.has("hass") || changedProps.has("resyncSetpoint"))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[this._config.entity];
|
||||
if (!stateObj) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
|
||||
|
||||
if (
|
||||
!oldHass ||
|
||||
oldHass.states[this._config.entity] !== stateObj ||
|
||||
(changedProps.has("resyncSetpoint") && this.resyncSetpoint)
|
||||
) {
|
||||
this._setTemp = this._getSetTemp(stateObj);
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
// 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 get _stepSize(): number {
|
||||
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
|
||||
if (stateObj.attributes.target_temp_step) {
|
||||
return stateObj.attributes.target_temp_step;
|
||||
}
|
||||
return this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5;
|
||||
}
|
||||
|
||||
private _getSetTemp(
|
||||
stateObj: HassEntity
|
||||
): undefined | number | [number, number] {
|
||||
if (stateObj.state === UNAVAILABLE) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (
|
||||
stateObj.attributes.target_temp_low &&
|
||||
stateObj.attributes.target_temp_high
|
||||
) {
|
||||
return [
|
||||
stateObj.attributes.target_temp_low,
|
||||
stateObj.attributes.target_temp_high,
|
||||
];
|
||||
}
|
||||
|
||||
return stateObj.attributes.temperature;
|
||||
}
|
||||
|
||||
private _dragEvent(e): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
|
||||
if (e.detail.low) {
|
||||
this._setTemp = [e.detail.low, stateObj.attributes.target_temp_high];
|
||||
} else if (e.detail.high) {
|
||||
this._setTemp = [stateObj.attributes.target_temp_low, e.detail.high];
|
||||
} else {
|
||||
this._setTemp = e.detail.value;
|
||||
}
|
||||
}
|
||||
|
||||
private _setTemperature(e): void {
|
||||
const stateObj = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
|
||||
if (e.detail.low) {
|
||||
const newVal = e.detail.low;
|
||||
this._callServiceHelper(
|
||||
stateObj.attributes.target_temp_low,
|
||||
newVal,
|
||||
"set_temperature",
|
||||
{
|
||||
target_temp_low: newVal,
|
||||
target_temp_high: stateObj.attributes.target_temp_high,
|
||||
}
|
||||
);
|
||||
} else if (e.detail.high) {
|
||||
const newVal = e.detail.high;
|
||||
this._callServiceHelper(
|
||||
stateObj.attributes.target_temp_high,
|
||||
newVal,
|
||||
"set_temperature",
|
||||
{
|
||||
target_temp_low: stateObj.attributes.target_temp_low,
|
||||
target_temp_high: newVal,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
const newVal = e.detail.value;
|
||||
this._callServiceHelper(
|
||||
stateObj!.attributes.temperature,
|
||||
newVal,
|
||||
"set_temperature",
|
||||
{ temperature: newVal }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _renderIcon(mode: string, currentMode: string) {
|
||||
if (!modeIcons[mode]) {
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
const stateObj = this.hass.states[this._config.entity] as ClimateEntity;
|
||||
|
||||
if (!stateObj) {
|
||||
return html`
|
||||
<hui-warning>
|
||||
${createEntityNotFoundWarning(this.hass, this._config.entity)}
|
||||
</hui-warning>
|
||||
`;
|
||||
}
|
||||
|
||||
const name = this._config!.name || computeStateName(stateObj);
|
||||
|
||||
const color = stateColorCss(stateObj);
|
||||
|
||||
return html`
|
||||
<ha-icon-button
|
||||
class=${classMap({ "selected-icon": currentMode === mode })}
|
||||
.mode=${mode}
|
||||
@click=${this._handleAction}
|
||||
tabindex="0"
|
||||
.path=${modeIcons[mode]}
|
||||
.label=${this.hass!.localize(
|
||||
`component.climate.entity_component._.state.${mode}`
|
||||
) || mode}
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-card>
|
||||
<p class="title">${name}</p>
|
||||
<ha-more-info-climate-temperature
|
||||
show-current
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
></ha-more-info-climate-temperature>
|
||||
<ha-icon-button
|
||||
class="more-info"
|
||||
.label=${this.hass!.localize(
|
||||
"ui.panel.lovelace.cards.show_more_info"
|
||||
)}
|
||||
.path=${mdiDotsVertical}
|
||||
@click=${this._handleMoreInfo}
|
||||
tabindex="0"
|
||||
></ha-icon-button>
|
||||
<hui-tile-features
|
||||
style=${styleMap({
|
||||
"--tile-color": color,
|
||||
})}
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.color=${this._config.color}
|
||||
.features=${this._config.features}
|
||||
></hui-tile-features>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleMoreInfo() {
|
||||
fireEvent(this, "hass-more-info", {
|
||||
entityId: this._config!.entity,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleAction(e: MouseEvent): void {
|
||||
this.hass!.callService("climate", "set_hvac_mode", {
|
||||
entity_id: this._config!.entity,
|
||||
hvac_mode: (e.currentTarget as any).mode,
|
||||
});
|
||||
}
|
||||
|
||||
private async _callServiceHelper(
|
||||
oldVal: unknown,
|
||||
newVal: unknown,
|
||||
service: string,
|
||||
data: {
|
||||
entity_id?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
) {
|
||||
if (oldVal === newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
data.entity_id = this._config!.entity;
|
||||
|
||||
await this.hass!.callService("climate", service, data);
|
||||
|
||||
// After updating temperature, wait 2s and check if the values
|
||||
// from call service are reflected in the entity. If not, resync
|
||||
// the slider to the entity values.
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 2000);
|
||||
});
|
||||
|
||||
const newState = this.hass!.states[this._config!.entity] as ClimateEntity;
|
||||
delete data.entity_id;
|
||||
|
||||
if (
|
||||
Object.entries(data).every(
|
||||
([key, value]) => newState.attributes[key] === value
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.resyncSetpoint = true;
|
||||
await this.updateComplete;
|
||||
this.resyncSetpoint = false;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
@ -523,10 +156,27 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
--name-font-size: 1.2rem;
|
||||
--brightness-font-size: 1.2rem;
|
||||
--rail-border-color: transparent;
|
||||
--mode-color: var(--state-inactive-color);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
padding: 12px 36px 16px 36px;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
ha-more-info-climate-temperature {
|
||||
width: 100%;
|
||||
max-width: 344px; /* 12px + 12px + 320px */
|
||||
padding: 0 12px 12px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.more-info {
|
||||
@ -538,93 +188,11 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard {
|
||||
inset-inline-start: initial;
|
||||
border-radius: 100%;
|
||||
color: var(--secondary-text-color);
|
||||
z-index: 1;
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
#controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#slider {
|
||||
height: 100%;
|
||||
hui-tile-features {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
max-width: 250px;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
round-slider {
|
||||
--round-slider-path-color: var(--slider-track-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;
|
||||
}
|
||||
|
||||
#temperature {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
top: 45%;
|
||||
left: 50%;
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -452,6 +452,7 @@ export interface ThermostatCardConfig extends LovelaceCardConfig {
|
||||
entity: string;
|
||||
theme?: string;
|
||||
name?: string;
|
||||
features?: LovelaceTileFeatureConfig[];
|
||||
}
|
||||
|
||||
export interface WeatherForecastCardConfig extends LovelaceCardConfig {
|
||||
|
@ -141,6 +141,12 @@ export const computeCards = (
|
||||
const cardConfig: ThermostatCardConfig = {
|
||||
type: "thermostat",
|
||||
entity: entityId,
|
||||
features: [
|
||||
{
|
||||
type: "climate-hvac-modes",
|
||||
hvac_modes: states[entityId]?.attributes?.hvac_modes,
|
||||
},
|
||||
],
|
||||
};
|
||||
cards.push(cardConfig);
|
||||
} else if (domain === "humidifier") {
|
||||
|
@ -1,13 +1,35 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { assert, assign, object, optional, string } from "superstruct";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
any,
|
||||
array,
|
||||
assert,
|
||||
assign,
|
||||
object,
|
||||
optional,
|
||||
string,
|
||||
} from "superstruct";
|
||||
import { HASSDomEvent, fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { ThermostatCardConfig } from "../../cards/types";
|
||||
import {
|
||||
LovelaceTileFeatureConfig,
|
||||
LovelaceTileFeatureContext,
|
||||
} from "../../tile-features/types";
|
||||
import type { LovelaceCardEditor } from "../../types";
|
||||
import "../hui-sub-element-editor";
|
||||
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
|
||||
import { EditSubElementEvent, SubElementEditorConfig } from "../types";
|
||||
import "./hui-tile-card-features-editor";
|
||||
import type { FeatureType } from "./hui-tile-card-features-editor";
|
||||
|
||||
const COMPATIBLE_FEATURES_TYPES: FeatureType[] = [
|
||||
"climate-hvac-modes",
|
||||
"climate-preset-modes",
|
||||
];
|
||||
|
||||
const cardConfigStruct = assign(
|
||||
baseLovelaceCardConfig,
|
||||
@ -15,6 +37,7 @@ const cardConfigStruct = assign(
|
||||
entity: optional(string()),
|
||||
name: optional(string()),
|
||||
theme: optional(string()),
|
||||
features: optional(array(any())),
|
||||
})
|
||||
);
|
||||
|
||||
@ -24,8 +47,8 @@ const SCHEMA = [
|
||||
type: "grid",
|
||||
name: "",
|
||||
schema: [
|
||||
{ name: "name", required: false, selector: { text: {} } },
|
||||
{ name: "theme", required: false, selector: { theme: {} } },
|
||||
{ name: "name", selector: { text: {} } },
|
||||
{ name: "theme", selector: { theme: {} } },
|
||||
],
|
||||
},
|
||||
] as const;
|
||||
@ -39,16 +62,39 @@ export class HuiThermostatCardEditor
|
||||
|
||||
@state() private _config?: ThermostatCardConfig;
|
||||
|
||||
@state() private _subElementEditorConfig?: SubElementEditorConfig;
|
||||
|
||||
public setConfig(config: ThermostatCardConfig): void {
|
||||
assert(config, cardConfigStruct);
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
private _context = memoizeOne(
|
||||
(entity_id?: string): LovelaceTileFeatureContext => ({ entity_id })
|
||||
);
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const stateObj = this._config.entity
|
||||
? this.hass.states[this._config.entity]
|
||||
: undefined;
|
||||
|
||||
if (this._subElementEditorConfig) {
|
||||
return html`
|
||||
<hui-sub-element-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._subElementEditorConfig}
|
||||
.context=${this._context(this._config.entity)}
|
||||
@go-back=${this._goBack}
|
||||
@config-changed=${this.subElementChanged}
|
||||
>
|
||||
</hui-sub-element-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
@ -57,6 +103,14 @@ export class HuiThermostatCardEditor
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
<hui-tile-card-features-editor
|
||||
.hass=${this.hass}
|
||||
.stateObj=${stateObj}
|
||||
.featuresTypes=${COMPATIBLE_FEATURES_TYPES}
|
||||
.features=${this._config!.features ?? []}
|
||||
@features-changed=${this._featuresChanged}
|
||||
@edit-detail-element=${this._editDetailElement}
|
||||
></hui-tile-card-features-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
@ -64,6 +118,62 @@ export class HuiThermostatCardEditor
|
||||
fireEvent(this, "config-changed", { config: ev.detail.value });
|
||||
}
|
||||
|
||||
private _featuresChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const features = ev.detail.features as LovelaceTileFeatureConfig[];
|
||||
const config: ThermostatCardConfig = {
|
||||
...this._config,
|
||||
features,
|
||||
};
|
||||
|
||||
if (features.length === 0) {
|
||||
delete config.features;
|
||||
}
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private subElementChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
if (!this._config || !this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = ev.detail.config;
|
||||
|
||||
const newConfigFeatures = this._config!.features
|
||||
? [...this._config!.features]
|
||||
: [];
|
||||
|
||||
if (!value) {
|
||||
newConfigFeatures.splice(this._subElementEditorConfig!.index!, 1);
|
||||
this._goBack();
|
||||
} else {
|
||||
newConfigFeatures[this._subElementEditorConfig!.index!] = value;
|
||||
}
|
||||
|
||||
this._config = { ...this._config!, features: newConfigFeatures };
|
||||
|
||||
this._subElementEditorConfig = {
|
||||
...this._subElementEditorConfig!,
|
||||
elementConfig: value,
|
||||
};
|
||||
|
||||
fireEvent(this, "config-changed", { config: this._config });
|
||||
}
|
||||
|
||||
private _editDetailElement(ev: HASSDomEvent<EditSubElementEvent>): void {
|
||||
this._subElementEditorConfig = ev.detail.subElementConfig;
|
||||
}
|
||||
|
||||
private _goBack(): void {
|
||||
this._subElementEditorConfig = undefined;
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
if (schema.name === "entity") {
|
||||
return this.hass!.localize(
|
||||
@ -71,18 +181,19 @@ export class HuiThermostatCardEditor
|
||||
);
|
||||
}
|
||||
|
||||
if (schema.name === "theme") {
|
||||
return `${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.generic.theme"
|
||||
)} (${this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.card.config.optional"
|
||||
)})`;
|
||||
}
|
||||
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.generic.${schema.name}`
|
||||
);
|
||||
};
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-form {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
@ -40,7 +40,7 @@ import { LovelaceTileFeatureConfig } from "../../tile-features/types";
|
||||
import { supportsClimatePresetModesTileFeature } from "../../tile-features/hui-climate-preset-modes-tile-feature";
|
||||
import { supportsNumberTileFeature } from "../../tile-features/hui-number-tile-feature";
|
||||
|
||||
type FeatureType = LovelaceTileFeatureConfig["type"];
|
||||
export type FeatureType = LovelaceTileFeatureConfig["type"];
|
||||
type SupportsFeature = (stateObj: HassEntity) => boolean;
|
||||
|
||||
const UI_FEATURE_TYPES = [
|
||||
@ -121,7 +121,11 @@ export class HuiTileCardFeaturesEditor extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public features?: LovelaceTileFeatureConfig[];
|
||||
|
||||
@property() public label?: string;
|
||||
@property({ attribute: false })
|
||||
public featuresTypes?: FeatureType[];
|
||||
|
||||
@property()
|
||||
public label?: string;
|
||||
|
||||
private _featuresKeys = new WeakMap<LovelaceTileFeatureConfig, string>();
|
||||
|
||||
@ -186,7 +190,9 @@ export class HuiTileCardFeaturesEditor extends LitElement {
|
||||
}
|
||||
|
||||
private _getSupportedFeaturesType() {
|
||||
const featuresTypes = UI_FEATURE_TYPES as readonly string[];
|
||||
const featuresTypes = UI_FEATURE_TYPES.filter(
|
||||
(type) => !this.featuresTypes || this.featuresTypes.includes(type)
|
||||
) as readonly string[];
|
||||
const customFeaturesTypes = customTileFeatures.map(
|
||||
(feature) => `${CUSTOM_TYPE_PREFIX}${feature.type}`
|
||||
);
|
||||
|
@ -23,7 +23,7 @@ export const supportsClimateHvacModesTileFeature = (stateObj: HassEntity) => {
|
||||
};
|
||||
|
||||
@customElement("hui-climate-hvac-modes-tile-feature")
|
||||
class HuiClimateHvacModeTileFeature
|
||||
class HuiClimateHvacModesTileFeature
|
||||
extends LitElement
|
||||
implements LovelaceTileFeature
|
||||
{
|
||||
@ -148,6 +148,6 @@ class HuiClimateHvacModeTileFeature
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-climate-modes-hvac-modes-feature": HuiClimateHvacModeTileFeature;
|
||||
"hui-climate-modes-hvac-modes-feature": HuiClimateHvacModesTileFeature;
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +106,10 @@
|
||||
"high": "high",
|
||||
"low": "low",
|
||||
"mode": "Mode",
|
||||
"preset": "Preset"
|
||||
"preset": "Preset",
|
||||
"target_label": "{action} to target",
|
||||
"target": "Target",
|
||||
"humidity_target": "Humidity target"
|
||||
},
|
||||
"counter": {
|
||||
"actions": {
|
||||
@ -128,6 +131,7 @@
|
||||
"reverse": "Reverse"
|
||||
},
|
||||
"humidifier": {
|
||||
"currently": "[%key:ui::card::climate::currently%]",
|
||||
"humidity": "Target humidity",
|
||||
"state": "State",
|
||||
"mode": "Mode",
|
||||
@ -135,7 +139,9 @@
|
||||
"current_humidity_entity": "{name} current humidity",
|
||||
"humidifying": "{name} humidifying",
|
||||
"drying": "{name} drying",
|
||||
"on_entity": "{name} on"
|
||||
"on_entity": "{name} on",
|
||||
"target_label": "[%key:ui::card::climate::target_label%]",
|
||||
"target": "[%key:ui::card::climate::target%]"
|
||||
},
|
||||
"lawn_mower": {
|
||||
"actions": {
|
||||
@ -226,11 +232,13 @@
|
||||
}
|
||||
},
|
||||
"water_heater": {
|
||||
"currently": "Currently",
|
||||
"currently": "[%key:ui::card::climate::currently%]",
|
||||
"on_off": "On / off",
|
||||
"target_temperature": "Target temperature",
|
||||
"away_mode": "Away mode",
|
||||
"mode": "Mode"
|
||||
"mode": "Mode",
|
||||
"target_label": "[%key:ui::card::climate::target_label%]",
|
||||
"target": "[%key:ui::card::climate::target%]"
|
||||
},
|
||||
"weather": {
|
||||
"attributes": {
|
||||
@ -1039,25 +1047,15 @@
|
||||
"unlock": "Unlock"
|
||||
},
|
||||
"climate": {
|
||||
"target_label": "{action} to target",
|
||||
"target": "Target",
|
||||
"humidity_target": "Humidity target",
|
||||
"temperature": "Temperature",
|
||||
"humidity": "Humidity"
|
||||
},
|
||||
"humidifier": {
|
||||
"target_label": "[%key:ui::dialogs::more_info_control::climate::target_label%]",
|
||||
"target": "[%key:ui::dialogs::more_info_control::climate::target%]"
|
||||
},
|
||||
"lawn_mower": {
|
||||
"activity": "Activity",
|
||||
"commands": "Lawn mower commands:",
|
||||
"start_mowing": "Start mowing",
|
||||
"pause": "Pause",
|
||||
"dock": "Return to dock"
|
||||
},
|
||||
"water_heater": {
|
||||
"target": "[%key:ui::dialogs::more_info_control::climate::target%]"
|
||||
}
|
||||
},
|
||||
"entity_registry": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user