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` +

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