diff --git a/src/components/ha-big-number.ts b/src/components/ha-big-number.ts new file mode 100644 index 0000000000..ee0a5cf759 --- /dev/null +++ b/src/components/ha-big-number.ts @@ -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` +

+ + ${formattedValue} +

+ `; + } + + 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; + } +} diff --git a/src/components/ha-control-circular-slider.ts b/src/components/ha-control-circular-slider.ts index fb2ef373d5..d6e72e838f 100644 --- a/src/components/ha-control-circular-slider.ts +++ b/src/components/ha-control-circular-slider.ts @@ -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 { diff --git a/src/components/ha-control-number-buttons.ts b/src/components/ha-control-number-buttons.ts index 771e9aee98..c68ddd9754 100644 --- a/src/components/ha-control-number-buttons.ts +++ b/src/components/ha-control-number-buttons.ts @@ -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; diff --git a/src/components/ha-control-select.ts b/src/components/ha-control-select.ts index 91c740d0e5..13341e617c 100644 --- a/src/components/ha-control-select.ts +++ b/src/components/ha-control-select.ts @@ -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; diff --git a/src/dialogs/more-info/components/climate/ha-more-info-climate-humidity.ts b/src/dialogs/more-info/components/climate/ha-more-info-climate-humidity.ts index 57344a4219..0040c44d9d 100644 --- a/src/dialogs/more-info/components/climate/ha-more-info-climate-humidity.ts +++ b/src/dialogs/more-info/components/climate/ha-more-info-climate-humidity.ts @@ -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`

- ${this.hass.localize( - "ui.dialogs.more_info_control.climate.humidity_target" - )} + ${this.hass.localize("ui.card.climate.humidity_target")}

`; } @@ -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` -
- -

${formatted}

-
+ + `; + } + + private _renderCurrentHumidity(humidity?: number) { + if (!this.showCurrent || humidity == null) { + return html`

 

`; + } + + return html` +

+ + + ${this.hass.formatEntityAttributeValue( + this.stateObj, + "current_humidity", + humidity + )} + +

`; } @@ -174,10 +194,10 @@ export class HaMoreInfoClimateHumidity extends LitElement { >
-
${this._renderLabel()}
-
- ${this._renderTarget(targetHumidity)} -
+ ${this._renderLabel()} ${this._renderTarget(targetHumidity)} + ${this._renderCurrentHumidity( + this.stateObj.attributes.current_humidity + )}
${this._renderButtons()} @@ -195,32 +215,17 @@ export class HaMoreInfoClimateHumidity extends LitElement { >
-
${this._renderLabel()}
+ ${this._renderLabel()} + ${this._renderCurrentHumidity( + this.stateObj.attributes.current_humidity + )}
`; } 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; } } diff --git a/src/dialogs/more-info/components/climate/ha-more-info-climate-temperature.ts b/src/dialogs/more-info/components/climate/ha-more-info-climate-temperature.ts index 333a301ad5..1dc4e87a1b 100644 --- a/src/dialogs/more-info/components/climate/ha-more-info-climate-temperature.ts +++ b/src/dialogs/more-info/components/climate/ha-more-info-climate-temperature.ts @@ -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> = {}; @state() private _selectTargetTemperature: Target = "low"; @@ -183,14 +179,9 @@ export class HaMoreInfoClimateTemperature extends LitElement { return html`

- ${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")}

`; } @@ -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` + + `; + } - const temperatureDecimal = formatted.replace(temperatureInteger, ""); + private _renderCurrentTemperature(temperature?: number) { + if (!this.showCurrent || temperature == null) { + return html`

 

`; + } return html` -

- - - ${this.stateObj.attributes.temperature} - ${this.hass.config.unit_system.temperature} +

+ + + ${this.hass.formatEntityAttributeValue( + this.stateObj, + "current_temperature", + temperature + )}

`; @@ -326,10 +321,11 @@ export class HaMoreInfoClimateTemperature extends LitElement { >
-
${this._renderLabel()}
-
- ${this._renderTargetTemperature(this._targetTemperature.value)} -
+ ${this._renderLabel()} + ${this._renderTargetTemperature(this._targetTemperature.value)} + ${this._renderCurrentTemperature( + this.stateObj.attributes.current_temperature + )}
${this._renderTemperatureButtons("value")} @@ -367,8 +363,8 @@ export class HaMoreInfoClimateTemperature extends LitElement { >
-
${this._renderLabel()}
-
+ ${this._renderLabel()} +
+ ${this._renderCurrentTemperature( + this.stateObj.attributes.current_temperature + )}
${this._renderTemperatureButtons(this._selectTargetTemperature, true)}
@@ -412,7 +411,10 @@ export class HaMoreInfoClimateTemperature extends LitElement { >
-
${this._renderLabel()}
+ ${this._renderLabel()} + ${this._renderCurrentTemperature( + this.stateObj.attributes.current_temperature + )}
`; @@ -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, diff --git a/src/dialogs/more-info/components/ha-more-info-control-circular-slider-style.ts b/src/dialogs/more-info/components/ha-more-info-control-circular-slider-style.ts index cee92177ff..89983772df 100644 --- a/src/dialogs/more-info/components/ha-more-info-control-circular-slider-style.ts +++ b/src/dialogs/more-info/components/ha-more-info-control-circular-slider-style.ts @@ -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 { diff --git a/src/dialogs/more-info/components/humidifier/ha-more-info-humidifier-humidity.ts b/src/dialogs/more-info/components/humidifier/ha-more-info-humidifier-humidity.ts index 612720228a..721d49c808 100644 --- a/src/dialogs/more-info/components/humidifier/ha-more-info-humidifier-humidity.ts +++ b/src/dialogs/more-info/components/humidifier/ha-more-info-humidifier-humidity.ts @@ -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`

${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")} +

+ `; + } + + private _renderCurrentHumidity(humidity?: number) { + if (!this.showCurrent || humidity == null) { + return html`

 

`; + } + + return html` +

+ ${this.hass.localize("ui.card.humidifier.currently")} + + ${this.hass.formatEntityAttributeValue( + this.stateObj, + "current_humidity", + humidity + )} +

`; } @@ -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` -
- -

- ${formatted}${blankBeforePercent(this.hass.locale)}% -

-
+ `; } @@ -191,10 +208,10 @@ export class HaMoreInfoHumidifierHumidity extends LitElement { >
-
${this._renderLabel()}
-
- ${this._renderTarget(targetHumidity)} -
+ ${this._renderLabel()} ${this._renderTarget(targetHumidity)} + ${this._renderCurrentHumidity( + this.stateObj.attributes.current_humidity + )}
${this._renderButtons()} @@ -217,32 +234,17 @@ export class HaMoreInfoHumidifierHumidity extends LitElement { >
-
${this._renderLabel()}
+ ${this._renderLabel()} + ${this._renderCurrentHumidity( + this.stateObj.attributes.current_humidity + )}
`; } 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; } } diff --git a/src/dialogs/more-info/components/water_heater/ha-more-info-water_heater-temperature.ts b/src/dialogs/more-info/components/water_heater/ha-more-info-water_heater-temperature.ts index 8ced14169a..3be4b2a2df 100644 --- a/src/dialogs/more-info/components/water_heater/ha-more-info-water_heater-temperature.ts +++ b/src/dialogs/more-info/components/water_heater/ha-more-info-water_heater-temperature.ts @@ -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` -

- ${this.hass.localize( - "ui.dialogs.more_info_control.water_heater.target" - )} -

+

${this.hass.localize("ui.card.water_heater.target")}

`; } @@ -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` + + `; + } - const temperatureDecimal = formatted.replace(temperatureInteger, ""); + private _renderCurrentTemperature(temperature?: number) { + if (!this.showCurrent || temperature == null) { + return html`

 

`; + } return html` -

- - - ${this.stateObj.attributes.temperature} - ${this.hass.config.unit_system.temperature} +

+ ${this.hass.localize("ui.card.water_heater.currently")} + + ${this.hass.formatEntityAttributeValue( + this.stateObj, + "current_temperature", + temperature + )}

`; @@ -202,10 +198,11 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement { >
-
${this._renderLabel()}
-
- ${this._renderTargetTemperature(this._targetTemperature)} -
+ ${this._renderLabel()} + ${this._renderTargetTemperature(this._targetTemperature)} + ${this._renderCurrentTemperature( + this.stateObj.attributes.current_temperature + )}
${this._renderButtons()} @@ -230,49 +227,17 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement { >
-
${this._renderLabel()}
+ ${this._renderLabel()} + ${this._renderCurrentTemperature( + this.stateObj.attributes.current_temperature + )}
`; } 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; } } diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts index 4e0407af5a..0ed4346f82 100644 --- a/src/panels/lovelace/cards/hui-thermostat-card.ts +++ b/src/panels/lovelace/cards/hui-thermostat-card.ts @@ -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 { @@ -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` - - ${createEntityNotFoundWarning(this.hass, this._config.entity)} - - `; - } - - 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` ` - : html` - - `; - - const currentTemperature = svg` - - - ${ - stateObj.state !== UNAVAILABLE && - stateObj.attributes.current_temperature != null && - !isNaN(stateObj.attributes.current_temperature) - ? svg` - ${formatNumber( - stateObj.attributes.current_temperature, - this.hass.locale - )} - - ${this.hass.config.unit_system.temperature} - - ` - : nothing - } - - - `; - - const setValues = svg` - - - - ${ - 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 - } - - - ${ - 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 - } - - - - `; - - return html` - - - -
-
-
- ${slider} -
-
${currentTemperature} ${setValues}
-
-
-
-
-
- ${(stateObj.attributes.hvac_modes || []) - .concat() - .sort(compareClimateHvacModes) - .map((modeItem) => this._renderIcon(modeItem, mode))} -
- ${name} -
-
-
- `; - } - - 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` + + ${createEntityNotFoundWarning(this.hass, this._config.entity)} + + `; + } + + const name = this._config!.name || computeStateName(stateObj); + + const color = stateColorCss(stateObj); + return html` - - + +

${name}

+ + + +
`; } - 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); } `; } diff --git a/src/panels/lovelace/cards/types.ts b/src/panels/lovelace/cards/types.ts index 5cb1c45301..c0adf7335c 100644 --- a/src/panels/lovelace/cards/types.ts +++ b/src/panels/lovelace/cards/types.ts @@ -452,6 +452,7 @@ export interface ThermostatCardConfig extends LovelaceCardConfig { entity: string; theme?: string; name?: string; + features?: LovelaceTileFeatureConfig[]; } export interface WeatherForecastCardConfig extends LovelaceCardConfig { diff --git a/src/panels/lovelace/common/generate-lovelace-config.ts b/src/panels/lovelace/common/generate-lovelace-config.ts index 300a713f0b..fdd03510a6 100644 --- a/src/panels/lovelace/common/generate-lovelace-config.ts +++ b/src/panels/lovelace/common/generate-lovelace-config.ts @@ -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") { diff --git a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts index 79158e892d..79e5e7c316 100644 --- a/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-thermostat-card-editor.ts @@ -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` + + + `; + } + return html` + `; } @@ -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): void { + this._subElementEditorConfig = ev.detail.subElementConfig; + } + + private _goBack(): void { + this._subElementEditorConfig = undefined; + } + private _computeLabelCallback = (schema: SchemaUnion) => { 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 { diff --git a/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts b/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts index 2c4a60b8b5..e1e556a0b1 100644 --- a/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts +++ b/src/panels/lovelace/editor/config-elements/hui-tile-card-features-editor.ts @@ -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(); @@ -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}` ); diff --git a/src/panels/lovelace/tile-features/hui-climate-hvac-modes-tile-feature.ts b/src/panels/lovelace/tile-features/hui-climate-hvac-modes-tile-feature.ts index c07020d041..4f7a32dda1 100644 --- a/src/panels/lovelace/tile-features/hui-climate-hvac-modes-tile-feature.ts +++ b/src/panels/lovelace/tile-features/hui-climate-hvac-modes-tile-feature.ts @@ -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; } } diff --git a/src/translations/en.json b/src/translations/en.json index 384bc8fe64..643116156e 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -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": {