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`
+
- ${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`
- > = {};
@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`
-
-
- ${temperatureInteger}
- ${digits !== 0
- ? html`${temperatureDecimal}`
- : nothing}
-
- ${this.hass.config.unit_system.temperature}
-
-
-
- ${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}%
-
-
- ${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`
-
-
- ${temperatureInteger}
- ${digits !== 0
- ? html`${temperatureDecimal}`
- : nothing}
-
- ${this.hass.config.unit_system.temperature}
-
-
-
- ${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`
-
- `;
-
- const setValues = svg`
-
- `;
-
- 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": {