diff --git a/gallery/src/pages/more-info/climate.markdown b/gallery/src/pages/more-info/climate.markdown
new file mode 100644
index 0000000000..dd60ba3702
--- /dev/null
+++ b/gallery/src/pages/more-info/climate.markdown
@@ -0,0 +1,3 @@
+---
+title: Climate
+---
diff --git a/gallery/src/pages/more-info/climate.ts b/gallery/src/pages/more-info/climate.ts
new file mode 100644
index 0000000000..95cc65321e
--- /dev/null
+++ b/gallery/src/pages/more-info/climate.ts
@@ -0,0 +1,83 @@
+import { html, LitElement, PropertyValues, TemplateResult } from "lit";
+import { customElement, property, query } from "lit/decorators";
+import "../../../../src/components/ha-card";
+import "../../../../src/dialogs/more-info/more-info-content";
+import { getEntity } from "../../../../src/fake_data/entity";
+import {
+ MockHomeAssistant,
+ provideHass,
+} from "../../../../src/fake_data/provide_hass";
+import "../../components/demo-more-infos";
+import { ClimateEntityFeature } from "../../../../src/data/climate";
+
+const ENTITIES = [
+ getEntity("climate", "thermostat", "heat", {
+ friendly_name: "Basic heater",
+ hvac_modes: ["heat", "off"],
+ hvac_mode: "heat",
+ current_temperature: 18,
+ temperature: 20,
+ min_temp: 10,
+ max_temp: 30,
+ supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
+ }),
+ getEntity("climate", "ac", "cool", {
+ friendly_name: "Basic air conditioning",
+ hvac_modes: ["cool", "off"],
+ hvac_mode: "cool",
+ current_temperature: 18,
+ temperature: 20,
+ min_temp: 10,
+ max_temp: 30,
+ supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
+ }),
+ getEntity("climate", "hvac", "auto", {
+ friendly_name: "Basic hvac",
+ hvac_modes: ["auto", "off"],
+ hvac_mode: "auto",
+ current_temperature: 18,
+ min_temp: 10,
+ max_temp: 30,
+ target_temp_step: 1,
+ supported_features: ClimateEntityFeature.TARGET_TEMPERATURE_RANGE,
+ target_temp_low: 20,
+ target_temp_high: 25,
+ }),
+ getEntity("climate", "unavailable", "unavailable", {
+ friendly_name: "Unavailable heater",
+ hvac_modes: ["heat", "off"],
+ hvac_mode: "heat",
+ min_temp: 10,
+ max_temp: 30,
+ supported_features: ClimateEntityFeature.TARGET_TEMPERATURE,
+ }),
+];
+
+@customElement("demo-more-info-climate")
+class DemoMoreInfoClimate extends LitElement {
+ @property() public hass!: MockHomeAssistant;
+
+ @query("demo-more-infos") private _demoRoot!: HTMLElement;
+
+ protected render(): TemplateResult {
+ return html`
+ ent.entityId)}
+ >
+ `;
+ }
+
+ protected firstUpdated(changedProperties: PropertyValues) {
+ super.firstUpdated(changedProperties);
+ const hass = provideHass(this._demoRoot);
+ hass.updateTranslations(null, "en");
+ hass.addEntities(ENTITIES);
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "demo-more-info-climate": DemoMoreInfoClimate;
+ }
+}
diff --git a/src/components/entity/state-badge.ts b/src/components/entity/state-badge.ts
index 1c91fce31b..17a9bcbbba 100644
--- a/src/components/entity/state-badge.ts
+++ b/src/components/entity/state-badge.ts
@@ -19,7 +19,7 @@ import {
} from "../../common/entity/state_color";
import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
-import { HVAC_ACTION_TO_MODE } from "../../data/climate";
+import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import type { HomeAssistant } from "../../types";
import "../ha-state-icon";
@@ -160,10 +160,10 @@ export class StateBadge extends LitElement {
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
- if (hvacAction in HVAC_ACTION_TO_MODE) {
+ if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
iconStyle.color = stateColorCss(
stateObj,
- HVAC_ACTION_TO_MODE[hvacAction]
+ CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
)!;
} else {
delete iconStyle.color;
diff --git a/src/components/ha-climate-control.ts b/src/components/ha-climate-control.ts
deleted file mode 100644
index b57cbb5053..0000000000
--- a/src/components/ha-climate-control.ts
+++ /dev/null
@@ -1,138 +0,0 @@
-import { mdiChevronDown, mdiChevronUp } from "@mdi/js";
-import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
-import { customElement, property, query } from "lit/decorators";
-import { fireEvent } from "../common/dom/fire_event";
-import { conditionalClamp } from "../common/number/clamp";
-import { HomeAssistant } from "../types";
-import "./ha-icon";
-import "./ha-icon-button";
-
-@customElement("ha-climate-control")
-class HaClimateControl extends LitElement {
- @property({ attribute: false }) public hass!: HomeAssistant;
-
- @property() public value!: number;
-
- @property() public unit = "";
-
- @property() public min?: number;
-
- @property() public max?: number;
-
- @property() public step = 1;
-
- private _lastChanged?: number;
-
- @query("#target_temperature") private _targetTemperature!: HTMLElement;
-
- protected render(): TemplateResult {
- return html`
-
${this.value} ${this.unit}
-
- `;
- }
-
- protected updated(changedProperties) {
- if (changedProperties.has("value")) {
- this._valueChanged();
- }
- }
-
- private _temperatureStateInFlux(inFlux) {
- this._targetTemperature.classList.toggle("in-flux", inFlux);
- }
-
- private _round(value) {
- // Round value to precision derived from step.
- // Inspired by https://github.com/soundar24/roundSlider/blob/master/src/roundslider.js
- const s = this.step.toString().split(".");
- return s[1] ? parseFloat(value.toFixed(s[1].length)) : Math.round(value);
- }
-
- private _incrementValue() {
- const newValue = this._round(this.value + this.step);
- this._processNewValue(newValue);
- }
-
- private _decrementValue() {
- const newValue = this._round(this.value - this.step);
- this._processNewValue(newValue);
- }
-
- private _processNewValue(value) {
- const newValue = conditionalClamp(value, this.min, this.max);
-
- if (this.value !== newValue) {
- this.value = newValue;
- this._lastChanged = Date.now();
- this._temperatureStateInFlux(true);
- }
- }
-
- private _valueChanged() {
- // When the last_changed timestamp is changed,
- // trigger a potential event fire in the future,
- // as long as last_changed is far enough in the past.
- if (this._lastChanged) {
- window.setTimeout(() => {
- const now = Date.now();
- if (now - this._lastChanged! >= 2000) {
- fireEvent(this, "change");
- this._temperatureStateInFlux(false);
- this._lastChanged = undefined;
- }
- }, 2010);
- }
- }
-
- static get styles(): CSSResultGroup {
- return css`
- :host {
- display: flex;
- justify-content: space-between;
- }
- .in-flux {
- color: var(--error-color);
- }
- #target_temperature {
- align-self: center;
- font-size: 28px;
- direction: ltr;
- }
- .control-buttons {
- font-size: 24px;
- text-align: right;
- }
- ha-icon-button {
- --mdc-icon-size: 32px;
- }
- `;
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- "ha-climate-control": HaClimateControl;
- }
-}
diff --git a/src/components/ha-control-circular-slider.ts b/src/components/ha-control-circular-slider.ts
index ffe26f17dc..b8bbffb336 100644
--- a/src/components/ha-control-circular-slider.ts
+++ b/src/components/ha-control-circular-slider.ts
@@ -409,6 +409,8 @@ export class HaControlCircularSlider extends LitElement {
value: number | undefined,
inverted: boolean | undefined
) {
+ if (this.disabled) return nothing;
+
const limit = inverted ? this.max : this.min;
const path = svgArc({
@@ -437,7 +439,10 @@ export class HaControlCircularSlider extends LitElement {
const targetCircleDashArray = this._strokeCircleDashArc(target);
const currentCircleDashArray =
- this.current != null && showActive
+ this.current != null &&
+ showActive &&
+ current <= this.max &&
+ current >= this.min
? this._strokeCircleDashArc(this.current)
: undefined;
@@ -474,17 +479,11 @@ export class HaControlCircularSlider extends LitElement {
@keydown=${this._handleKeyDown}
@keyup=${this._handleKeyUp}
/>
-
${
currentCircleDashArray
? svg`
`;
}
@@ -633,8 +638,8 @@ export class HaControlCircularSlider extends LitElement {
fill: none;
stroke-linecap: round;
stroke-width: 8px;
- stroke: white;
- opacity: 0.6;
+ stroke: var(--primary-text-color);
+ opacity: 0.5;
transition:
stroke-width 300ms ease-in-out,
stroke-dasharray 300ms ease-in-out,
@@ -643,6 +648,10 @@ export class HaControlCircularSlider extends LitElement {
opacity 180ms ease-in-out;
}
+ .arc-current {
+ stroke: var(--clear-background-color);
+ }
+
.arc-clear {
stroke: var(--clear-background-color);
}
diff --git a/src/data/climate.ts b/src/data/climate.ts
index 705f83fe15..ede93a6933 100644
--- a/src/data/climate.ts
+++ b/src/data/climate.ts
@@ -1,3 +1,14 @@
+import {
+ mdiClockOutline,
+ mdiFan,
+ mdiFire,
+ mdiHeatWave,
+ mdiPower,
+ mdiSnowflake,
+ mdiSunSnowflakeVariant,
+ mdiThermostatAuto,
+ mdiWaterPercent,
+} from "@mdi/js";
import {
HassEntityAttributeBase,
HassEntityBase,
@@ -74,7 +85,7 @@ const hvacModeOrdering: { [key in HvacMode]: number } = {
export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) =>
hvacModeOrdering[mode1] - hvacModeOrdering[mode2];
-export const HVAC_ACTION_TO_MODE: Record = {
+export const CLIMATE_HVAC_ACTION_TO_MODE: Record = {
cooling: "cool",
drying: "dry",
fan: "fan_only",
@@ -83,3 +94,23 @@ export const HVAC_ACTION_TO_MODE: Record = {
idle: "off",
off: "off",
};
+
+export const CLIMATE_HVAC_ACTION_ICONS: Record = {
+ cooling: mdiSnowflake,
+ drying: mdiWaterPercent,
+ fan: mdiFan,
+ heating: mdiFire,
+ idle: mdiClockOutline,
+ off: mdiPower,
+ preheating: mdiHeatWave,
+};
+
+export const CLIMATE_HVAC_MODE_ICONS: Record = {
+ cool: mdiSnowflake,
+ dry: mdiWaterPercent,
+ fan_only: mdiFan,
+ auto: mdiThermostatAuto,
+ heat: mdiFire,
+ off: mdiPower,
+ heat_cool: mdiSunSnowflakeVariant,
+};
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
new file mode 100644
index 0000000000..be43b3b796
--- /dev/null
+++ b/src/dialogs/more-info/components/climate/ha-more-info-climate-temperature.ts
@@ -0,0 +1,534 @@
+import { mdiMinus, mdiPlus } from "@mdi/js";
+import {
+ CSSResultGroup,
+ LitElement,
+ PropertyValues,
+ css,
+ html,
+ nothing,
+} from "lit";
+import { customElement, property, state } from "lit/decorators";
+import { classMap } from "lit/directives/class-map";
+import { styleMap } from "lit/directives/style-map";
+import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
+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-control-circular-slider";
+import "../../../../components/ha-outlined-icon-button";
+import "../../../../components/ha-svg-icon";
+import {
+ CLIMATE_HVAC_ACTION_TO_MODE,
+ ClimateEntity,
+ ClimateEntityFeature,
+} from "../../../../data/climate";
+import { UNAVAILABLE } from "../../../../data/entity";
+import { HomeAssistant } from "../../../../types";
+
+type Target = "value" | "low" | "high";
+
+@customElement("ha-more-info-climate-temperature")
+export class HaMoreInfoClimateTemperature extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property({ attribute: false }) public stateObj!: ClimateEntity;
+
+ @state() private _targetTemperature: Partial> = {};
+
+ @state() private _selectTargetTemperature: Target = "low";
+
+ protected willUpdate(changedProp: PropertyValues): void {
+ super.willUpdate(changedProp);
+ if (changedProp.has("stateObj")) {
+ this._targetTemperature = {
+ value: this.stateObj.attributes.temperature,
+ low: this.stateObj.attributes.target_temp_low,
+ high: this.stateObj.attributes.target_temp_high,
+ };
+ }
+ }
+
+ private get _step() {
+ return (
+ this.stateObj.attributes.target_temp_step ||
+ (this.hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1)
+ );
+ }
+
+ private get _min() {
+ return this.stateObj.attributes.min_temp;
+ }
+
+ private get _max() {
+ return this.stateObj.attributes.max_temp;
+ }
+
+ private _valueChanged(ev: CustomEvent) {
+ const value = (ev.detail as any).value;
+ if (isNaN(value)) return;
+ const target = ev.type.replace("-changed", "");
+ this._targetTemperature = {
+ ...this._targetTemperature,
+ [target]: value,
+ };
+ this._selectTargetTemperature = target as Target;
+ this._callService(target);
+ }
+
+ private _valueChanging(ev: CustomEvent) {
+ const value = (ev.detail as any).value;
+ if (isNaN(value)) return;
+ const target = ev.type.replace("-changing", "");
+ this._targetTemperature = {
+ ...this._targetTemperature,
+ [target]: value,
+ };
+ this._selectTargetTemperature = target as Target;
+ }
+
+ private _debouncedCallService = debounce(
+ (target: Target) => this._callService(target),
+ 1000
+ );
+
+ private _callService(type: string) {
+ if (type === "high" || type === "low") {
+ this.hass.callService("climate", "set_temperature", {
+ entity_id: this.stateObj!.entity_id,
+ target_temp_low: this._targetTemperature.low,
+ target_temp_high: this._targetTemperature.high,
+ });
+ return;
+ }
+ this.hass.callService("climate", "set_temperature", {
+ entity_id: this.stateObj!.entity_id,
+ temperature: this._targetTemperature.value,
+ });
+ }
+
+ private _handleButton(ev) {
+ const target = ev.currentTarget.target as Target;
+ const step = ev.currentTarget.step as number;
+
+ const defaultValue = target === "high" ? this._max : this._min;
+
+ let temp = this._targetTemperature[target] ?? defaultValue;
+ temp += step;
+ temp = clamp(temp, this._min, this._max);
+ if (target === "high" && this._targetTemperature.low != null) {
+ temp = clamp(temp, this._targetTemperature.low, this._max);
+ }
+ if (target === "low" && this._targetTemperature.high != null) {
+ temp = clamp(temp, this._min, this._targetTemperature.high);
+ }
+
+ this._targetTemperature = {
+ ...this._targetTemperature,
+ [target]: temp,
+ };
+ this._debouncedCallService(target);
+ }
+
+ private _handleSelectTemp(ev) {
+ const target = ev.currentTarget.target as Target;
+ this._selectTargetTemperature = target;
+ }
+
+ private _renderHvacAction() {
+ const action = this.stateObj.attributes.hvac_action;
+
+ const actionLabel = computeAttributeValueDisplay(
+ this.hass.localize,
+ this.stateObj,
+ this.hass.locale,
+ this.hass.config,
+ this.hass.entities,
+ "hvac_action"
+ ) as string;
+
+ 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")}
+
+ `;
+ }
+
+ private _renderTemperatureButtons(target: Target, colored?: boolean) {
+ const lowColor = stateColorCss(this.stateObj, "heat");
+ const highColor = stateColorCss(this.stateObj, "cool");
+
+ const color = colored
+ ? target === "high"
+ ? highColor
+ : lowColor
+ : undefined;
+
+ return html`
+
+
+
+
+
+
+
+
+ `;
+ }
+
+ private _renderTargetTemperature(temperature: number) {
+ const digits = this._step.toString().split(".")?.[1]?.length ?? 0;
+ const formatted = formatNumber(temperature, this.hass.locale, {
+ maximumFractionDigits: digits,
+ minimumFractionDigits: digits,
+ });
+ const [temperatureInteger] = formatted.includes(".")
+ ? formatted.split(".")
+ : formatted.split(",");
+
+ const temperatureDecimal = formatted.replace(temperatureInteger, "");
+
+ return html`
+
+
+ ${temperatureInteger}
+ ${digits !== 0
+ ? html`${temperatureDecimal}`
+ : nothing}
+
+ ${this.hass.config.unit_system.temperature}
+
+
+
+ ${this.stateObj.attributes.temperature}
+ ${this.hass.config.unit_system.temperature}
+
+
+ `;
+ }
+
+ protected render() {
+ const supportsTargetTemperature = supportsFeature(
+ this.stateObj,
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ );
+
+ const supportsTargetTemperatureRange = supportsFeature(
+ this.stateObj,
+ ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
+ );
+
+ const mode = this.stateObj.state;
+ const action = this.stateObj.attributes.hvac_action;
+ const active = stateActive(this.stateObj);
+
+ const mainColor = stateColorCss(this.stateObj);
+ const lowColor = stateColorCss(this.stateObj, active ? "heat" : "off");
+ const highColor = stateColorCss(this.stateObj, active ? "cool" : "off");
+
+ let actionColor: string | undefined;
+ if (action && action !== "idle" && action !== "off" && active) {
+ actionColor = stateColorCss(
+ this.stateObj,
+ CLIMATE_HVAC_ACTION_TO_MODE[action]
+ );
+ }
+
+ const hvacModes = this.stateObj.attributes.hvac_modes;
+
+ if (supportsTargetTemperature && this._targetTemperature.value != null) {
+ const hasOnlyCoolMode =
+ hvacModes.length === 2 &&
+ hvacModes.includes("cool") &&
+ hvacModes.includes("off");
+ return html`
+
+
+
+
+
${this._renderHvacAction()}
+
+ ${this._renderTargetTemperature(this._targetTemperature.value)}
+
+
+ ${this._renderTemperatureButtons("value")}
+
+ `;
+ }
+
+ if (
+ supportsTargetTemperatureRange &&
+ this._targetTemperature.low != null &&
+ this._targetTemperature.high != null
+ ) {
+ return html`
+
+
+
+
+
${this._renderHvacAction()}
+
+
+
+
+
+ ${this._renderTemperatureButtons(this._selectTargetTemperature, true)}
+
+ `;
+ }
+
+ return html`
+
+
+
+
+ `;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ /* Layout */
+ .container {
+ position: relative;
+ }
+ .info {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none;
+ font-size: 16px;
+ line-height: 24px;
+ letter-spacing: 0.1px;
+ }
+ .info * {
+ margin: 0;
+ pointer-events: auto;
+ }
+ /* 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 .unit {
+ font-size: 24px;
+ line-height: 40px;
+ }
+ .temperature .decimal {
+ font-size: 24px;
+ line-height: 40px;
+ align-self: flex-end;
+ margin-right: -18px;
+ }
+ .action-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 200px;
+ height: 48px;
+ margin-bottom: 6px;
+ }
+ .action {
+ font-weight: 500;
+ text-align: center;
+ color: var(--action-color, inherit);
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+ }
+ .dual {
+ display: flex;
+ flex-direction: row;
+ gap: 24px;
+ margin-bottom: 40px;
+ }
+
+ .dual button {
+ outline: none;
+ background: none;
+ color: inherit;
+ font-family: inherit;
+ -webkit-tap-highlight-color: transparent;
+ border: none;
+ opacity: 0.5;
+ padding: 0;
+ transition:
+ opacity 180ms ease-in-out,
+ transform 180ms ease-in-out;
+ cursor: pointer;
+ }
+ .dual button:focus-visible {
+ transform: scale(1.1);
+ }
+ .dual button.selected {
+ opacity: 1;
+ }
+ .buttons {
+ position: absolute;
+ bottom: 10px;
+ left: 0;
+ right: 0;
+ margin: 0 auto;
+ width: 120px;
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: space-between;
+ }
+ .buttons ha-outlined-icon-button {
+ --md-outlined-icon-button-container-size: 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;
+ }
+ /* Slider */
+ ha-control-circular-slider {
+ --control-circular-slider-color: var(
+ --main-color,
+ var(--disabled-color)
+ );
+ --control-circular-slider-low-color: var(
+ --low-color,
+ var(--disabled-color)
+ );
+ --control-circular-slider-high-color: var(
+ --high-color,
+ var(--disabled-color)
+ );
+ }
+ ha-control-circular-slider::after {
+ display: block;
+ content: "";
+ position: absolute;
+ top: -10%;
+ left: -10%;
+ right: -10%;
+ bottom: -10%;
+ background: radial-gradient(
+ 50% 50% at 50% 50%,
+ var(--action-color, transparent) 0%,
+ transparent 100%
+ );
+ opacity: 0.15;
+ pointer-events: none;
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-more-info-climate-temperature": HaMoreInfoClimateTemperature;
+ }
+}
diff --git a/src/dialogs/more-info/const.ts b/src/dialogs/more-info/const.ts
index a0dc211937..37295e1767 100644
--- a/src/dialogs/more-info/const.ts
+++ b/src/dialogs/more-info/const.ts
@@ -19,6 +19,7 @@ export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
export const DOMAINS_WITH_NEW_MORE_INFO = [
"alarm_control_panel",
"cover",
+ "climate",
"fan",
"input_boolean",
"light",
diff --git a/src/dialogs/more-info/controls/more-info-climate.ts b/src/dialogs/more-info/controls/more-info-climate.ts
index 9294e06855..962bb6e287 100644
--- a/src/dialogs/more-info/controls/more-info-climate.ts
+++ b/src/dialogs/more-info/controls/more-info-climate.ts
@@ -1,10 +1,10 @@
import "@material/mwc-list/mwc-list-item";
import {
- css,
CSSResultGroup,
- html,
LitElement,
PropertyValues,
+ css,
+ html,
nothing,
} from "lit";
import { property } from "lit/decorators";
@@ -17,8 +17,9 @@ import {
} from "../../../common/entity/compute_attribute_display";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { supportsFeature } from "../../../common/entity/supports-feature";
+import { formatNumber } from "../../../common/number/format_number";
+import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
import { computeRTLDirection } from "../../../common/util/compute_rtl";
-import "../../../components/ha-climate-control";
import "../../../components/ha-select";
import "../../../components/ha-slider";
import "../../../components/ha-switch";
@@ -28,6 +29,8 @@ import {
compareClimateHvacModes,
} from "../../../data/climate";
import { HomeAssistant } from "../../../types";
+import "../components/climate/ha-more-info-climate-temperature";
+import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
class MoreInfoClimate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -73,13 +76,60 @@ class MoreInfoClimate extends LitElement {
ClimateEntityFeature.AUX_HEAT
);
- const temperatureStepSize =
- stateObj.attributes.target_temp_step ||
- (hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1);
+ const currentTemperature = this.stateObj.attributes.current_temperature;
+ const currentHumidity = this.stateObj.attributes.current_humidity;
const rtlDirection = computeRTLDirection(hass);
return html`
+ ${currentTemperature || currentHumidity
+ ? html`
+ ${currentTemperature != null
+ ? html`
+
+
+ ${computeAttributeNameDisplay(
+ this.hass.localize,
+ this.stateObj,
+ this.hass.entities,
+ "current_temperature"
+ )}
+
+
+ ${formatNumber(currentTemperature, this.hass.locale)}
+ ${this.hass.config.unit_system.temperature}
+
+
+ `
+ : nothing}
+ ${currentHumidity != null
+ ? html`
+
+
+ ${computeAttributeNameDisplay(
+ this.hass.localize,
+ this.stateObj,
+ this.hass.entities,
+ "current_humidity"
+ )}
+
+
+ ${formatNumber(
+ currentHumidity,
+ this.hass.locale
+ )}${blankBeforePercent(this.hass.locale)}%
+
+
+ `
+ : nothing}
+
`
+ : nothing}
+
+
+
-
-
- ${supportTargetTemperature || supportTargetTemperatureRange
- ? html`
-
- ${computeAttributeNameDisplay(
- hass.localize,
- stateObj,
- hass.entities,
- "temperature"
- )}
-
- `
- : ""}
- ${stateObj.attributes.temperature !== undefined &&
- stateObj.attributes.temperature !== null
- ? html`
-
- `
- : ""}
- ${(stateObj.attributes.target_temp_low !== undefined &&
- stateObj.attributes.target_temp_low !== null) ||
- (stateObj.attributes.target_temp_high !== undefined &&
- stateObj.attributes.target_temp_high !== null)
- ? html`
-
-
- `
- : ""}
-
-
-
${supportTargetHumidity
? html`
@@ -184,34 +176,32 @@ class MoreInfoClimate extends LitElement {
: ""}
-
-
- ${stateObj.attributes.hvac_modes
- .concat()
- .sort(compareClimateHvacModes)
- .map(
- (mode) => html`
-
- ${computeStateDisplay(
- hass.localize,
- stateObj,
- hass.locale,
- this.hass.config,
- hass.entities,
- mode
- )}
-
- `
- )}
-
-
+
+ ${stateObj.attributes.hvac_modes
+ .concat()
+ .sort(compareClimateHvacModes)
+ .map(
+ (mode) => html`
+
+ ${computeStateDisplay(
+ hass.localize,
+ stateObj,
+ hass.locale,
+ this.hass.config,
+ hass.entities,
+ mode
+ )}
+
+ `
+ )}
+
${supportPresetMode && stateObj.attributes.preset_modes
@@ -358,42 +348,6 @@ class MoreInfoClimate extends LitElement {
}, 500);
}
- private _targetTemperatureChanged(ev) {
- const newVal = ev.target.value;
- this._callServiceHelper(
- this.stateObj!.attributes.temperature,
- newVal,
- "set_temperature",
- { temperature: newVal }
- );
- }
-
- private _targetTemperatureLowChanged(ev) {
- const newVal = ev.currentTarget.value;
- this._callServiceHelper(
- this.stateObj!.attributes.target_temp_low,
- newVal,
- "set_temperature",
- {
- target_temp_low: newVal,
- target_temp_high: this.stateObj!.attributes.target_temp_high,
- }
- );
- }
-
- private _targetTemperatureHighChanged(ev) {
- const newVal = ev.currentTarget.value;
- this._callServiceHelper(
- this.stateObj!.attributes.target_temp_high,
- newVal,
- "set_temperature",
- {
- target_temp_low: this.stateObj!.attributes.target_temp_low,
- target_temp_high: newVal,
- }
- );
- }
-
private _targetHumiditySliderChanged(ev) {
const newVal = ev.target.value;
this._callServiceHelper(
@@ -492,48 +446,76 @@ class MoreInfoClimate extends LitElement {
}
static get styles(): CSSResultGroup {
- return css`
- :host {
- color: var(--primary-text-color);
- }
+ return [
+ moreInfoControlStyle,
+ css`
+ :host {
+ color: var(--primary-text-color);
+ }
- ha-select {
- width: 100%;
- margin-top: 8px;
- }
+ .current {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ margin-bottom: 40px;
+ }
- ha-slider {
- width: 100%;
- }
+ .current div {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ flex: 1;
+ }
- .container-humidity .single-row {
- display: flex;
- height: 50px;
- }
+ .current p {
+ margin: 0;
+ text-align: center;
+ color: var(--primary-text-color);
+ }
- .target-humidity {
- width: 90px;
- font-size: 200%;
- margin: auto;
- direction: ltr;
- }
+ .current .label {
+ opacity: 0.8;
+ font-size: 14px;
+ line-height: 16px;
+ letter-spacing: 0.4px;
+ margin-bottom: 4px;
+ }
- ha-climate-control.range-control-left,
- ha-climate-control.range-control-right {
- float: left;
- width: 46%;
- }
- ha-climate-control.range-control-left {
- margin-right: 4%;
- }
- ha-climate-control.range-control-right {
- margin-left: 4%;
- }
+ .current .value {
+ font-size: 22px;
+ font-weight: 500;
+ line-height: 28px;
+ }
+ ha-select {
+ width: 100%;
+ margin-top: 8px;
+ }
- .single-row {
- padding: 8px 0;
- }
- `;
+ ha-slider {
+ width: 100%;
+ }
+
+ .container-humidity .single-row {
+ display: flex;
+ height: 50px;
+ }
+
+ .target-humidity {
+ width: 90px;
+ font-size: 200%;
+ margin: auto;
+ direction: ltr;
+ }
+
+ .single-row {
+ padding: 8px 0;
+ }
+ `,
+ ];
}
}
diff --git a/src/fake_data/entity.ts b/src/fake_data/entity.ts
index c7c31d5702..de4b0ca5a5 100644
--- a/src/fake_data/entity.ts
+++ b/src/fake_data/entity.ts
@@ -3,6 +3,8 @@ import {
HassEntity,
HassEntityAttributeBase,
} from "home-assistant-js-websocket";
+import { supportsFeature } from "../common/entity/supports-feature";
+import { ClimateEntityFeature } from "../data/climate";
const now = () => new Date().toISOString();
const randomTime = () =>
@@ -313,6 +315,45 @@ class ClimateEntity extends Entity {
super.handleService(domain, service, data);
}
}
+
+ public toState() {
+ const state = super.toState();
+
+ state.attributes.hvac_action = undefined;
+
+ if (
+ supportsFeature(
+ state as HassEntity,
+ ClimateEntityFeature.TARGET_TEMPERATURE
+ )
+ ) {
+ const current = state.attributes.current_temperature;
+ const target = state.attributes.temperature;
+ if (state.state === "heat") {
+ state.attributes.hvac_action = target >= current ? "heating" : "idle";
+ }
+ if (state.state === "cool") {
+ state.attributes.hvac_action = target <= current ? "cooling" : "idle";
+ }
+ }
+ if (
+ supportsFeature(
+ state as HassEntity,
+ ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
+ )
+ ) {
+ const current = state.attributes.current_temperature;
+ const lowTarget = state.attributes.target_temp_low;
+ const highTarget = state.attributes.target_temp_high;
+ state.attributes.hvac_action =
+ lowTarget >= current
+ ? "heating"
+ : highTarget <= current
+ ? "cooling"
+ : "idle";
+ }
+ return state;
+ }
}
class GroupEntity extends Entity {
diff --git a/src/panels/lovelace/cards/hui-button-card.ts b/src/panels/lovelace/cards/hui-button-card.ts
index 670e69b0e9..9cb0b359b1 100644
--- a/src/panels/lovelace/cards/hui-button-card.ts
+++ b/src/panels/lovelace/cards/hui-button-card.ts
@@ -34,7 +34,7 @@ import { isValidEntityId } from "../../../common/entity/valid_entity_id";
import { iconColorCSS } from "../../../common/style/icon_color_css";
import { LocalizeFunc } from "../../../common/translations/localize";
import "../../../components/ha-card";
-import { HVAC_ACTION_TO_MODE } from "../../../data/climate";
+import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate";
import {
configContext,
entitiesContext,
@@ -343,8 +343,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
}
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
- if (hvacAction in HVAC_ACTION_TO_MODE) {
- return stateColorCss(stateObj, HVAC_ACTION_TO_MODE[hvacAction]);
+ if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
+ return stateColorCss(stateObj, CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]);
}
return undefined;
}
diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts
index 201c4b9dd6..ca3ffdeaf0 100644
--- a/src/panels/lovelace/cards/hui-entity-card.ts
+++ b/src/panels/lovelace/cards/hui-entity-card.ts
@@ -29,7 +29,7 @@ import {
import { iconColorCSS } from "../../../common/style/icon_color_css";
import "../../../components/ha-card";
import "../../../components/ha-icon";
-import { HVAC_ACTION_TO_MODE } from "../../../data/climate";
+import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate";
import { isUnavailableState } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { computeCardSize } from "../common/compute-card-size";
@@ -201,8 +201,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
private _computeColor(stateObj: HassEntity): string | undefined {
if (stateObj.attributes.hvac_action) {
const hvacAction = stateObj.attributes.hvac_action;
- if (hvacAction in HVAC_ACTION_TO_MODE) {
- return stateColorCss(stateObj, HVAC_ACTION_TO_MODE[hvacAction]);
+ if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
+ return stateColorCss(stateObj, CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]);
}
return undefined;
}
diff --git a/src/panels/lovelace/cards/hui-thermostat-card.ts b/src/panels/lovelace/cards/hui-thermostat-card.ts
index b48978274c..f26273f4ee 100644
--- a/src/panels/lovelace/cards/hui-thermostat-card.ts
+++ b/src/panels/lovelace/cards/hui-thermostat-card.ts
@@ -47,6 +47,7 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
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,
diff --git a/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts b/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts
index 03c959eec6..a00d773e56 100644
--- a/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts
+++ b/src/panels/lovelace/cards/tile/badges/tile-badge-climate.ts
@@ -1,40 +1,11 @@
-import {
- mdiClockOutline,
- mdiFan,
- mdiFire,
- mdiHeatWave,
- mdiPower,
- mdiSnowflake,
- mdiWaterPercent,
-} from "@mdi/js";
import { stateColorCss } from "../../../../../common/entity/state_color";
import {
+ CLIMATE_HVAC_ACTION_ICONS,
+ CLIMATE_HVAC_ACTION_TO_MODE,
ClimateEntity,
- HvacAction,
- HvacMode,
} from "../../../../../data/climate";
import { ComputeBadgeFunction } from "./tile-badge";
-export const CLIMATE_HVAC_ACTION_ICONS: Record
= {
- cooling: mdiSnowflake,
- drying: mdiWaterPercent,
- fan: mdiFan,
- heating: mdiFire,
- idle: mdiClockOutline,
- off: mdiPower,
- preheating: mdiHeatWave,
-};
-
-export const CLIMATE_HVAC_ACTION_MODE: Record = {
- cooling: "cool",
- drying: "dry",
- fan: "fan_only",
- heating: "heat",
- idle: "off",
- off: "off",
- preheating: "heat",
-};
-
export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {
const hvacAction = (stateObj as ClimateEntity).attributes.hvac_action;
@@ -44,6 +15,6 @@ export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {
return {
iconPath: CLIMATE_HVAC_ACTION_ICONS[hvacAction],
- color: stateColorCss(stateObj, CLIMATE_HVAC_ACTION_MODE[hvacAction]),
+ color: stateColorCss(stateObj, CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]),
};
};
diff --git a/src/resources/ha-style.ts b/src/resources/ha-style.ts
index b8a039d3d6..7536f9f87c 100644
--- a/src/resources/ha-style.ts
+++ b/src/resources/ha-style.ts
@@ -109,7 +109,7 @@ documentContainer.innerHTML = `
--yellow-color: #ffeb3b;
--amber-color: #ffc107;
--orange-color: #ff9800;
- --deep-orange-color: #ff5722;
+ --deep-orange-color: #ff6f22;
--brown-color: #795548;
--light-grey-color: #bdbdbd;
--grey-color: #9e9e9e;
diff --git a/src/translations/en.json b/src/translations/en.json
index c6a6625844..7eb6025147 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -996,6 +996,10 @@
"open": "Open",
"lock": "Lock",
"unlock": "Unlock"
+ },
+ "climate": {
+ "target_label": "{action} to target",
+ "target": "Target"
}
},
"entity_registry": {