mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +00:00
Add new temperature control in climate more info (#17002)
* Add circular slider as temperature control * Move climate icons and mode mapping * Update icon * Add mode icon * Improve colors * Add temperature control buttons * Call service * Remove climate control * Some fixes * Add current temp and humidity * Fix default mode * Swap state and current * Some adjustments * prettier * Simplify color rules * refactor cool mode * Color button when dual climate * Add current temp and humidity * Fix opacity * Hide current temp is below min or above max * Adjust button size * Add action label * Better off and unavailable state * Improve current color * Add gallery * Fix dark mode * Add overflow * Update src/dialogs/more-info/controls/more-info-climate.ts Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Update src/panels/lovelace/cards/hui-thermostat-card.ts Co-authored-by: Bram Kragten <mail@bramkragten.nl> * Update src/dialogs/more-info/components/climate/ha-more-info-climate-temperature.ts --------- Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
parent
85733655c2
commit
89e96e4681
3
gallery/src/pages/more-info/climate.markdown
Normal file
3
gallery/src/pages/more-info/climate.markdown
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
title: Climate
|
||||||
|
---
|
83
gallery/src/pages/more-info/climate.ts
Normal file
83
gallery/src/pages/more-info/climate.ts
Normal file
@ -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`
|
||||||
|
<demo-more-infos
|
||||||
|
.hass=${this.hass}
|
||||||
|
.entities=${ENTITIES.map((ent) => ent.entityId)}
|
||||||
|
></demo-more-infos>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -19,7 +19,7 @@ import {
|
|||||||
} from "../../common/entity/state_color";
|
} from "../../common/entity/state_color";
|
||||||
import { iconColorCSS } from "../../common/style/icon_color_css";
|
import { iconColorCSS } from "../../common/style/icon_color_css";
|
||||||
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
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 type { HomeAssistant } from "../../types";
|
||||||
import "../ha-state-icon";
|
import "../ha-state-icon";
|
||||||
|
|
||||||
@ -160,10 +160,10 @@ export class StateBadge extends LitElement {
|
|||||||
}
|
}
|
||||||
if (stateObj.attributes.hvac_action) {
|
if (stateObj.attributes.hvac_action) {
|
||||||
const hvacAction = 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(
|
iconStyle.color = stateColorCss(
|
||||||
stateObj,
|
stateObj,
|
||||||
HVAC_ACTION_TO_MODE[hvacAction]
|
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
|
||||||
)!;
|
)!;
|
||||||
} else {
|
} else {
|
||||||
delete iconStyle.color;
|
delete iconStyle.color;
|
||||||
|
@ -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`
|
|
||||||
<div id="target_temperature">${this.value} ${this.unit}</div>
|
|
||||||
<div class="control-buttons">
|
|
||||||
<div>
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiChevronUp}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.climate-control.temperature_up"
|
|
||||||
)}
|
|
||||||
@click=${this._incrementValue}
|
|
||||||
>
|
|
||||||
</ha-icon-button>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<ha-icon-button
|
|
||||||
.path=${mdiChevronDown}
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.components.climate-control.temperature_down"
|
|
||||||
)}
|
|
||||||
@click=${this._decrementValue}
|
|
||||||
>
|
|
||||||
</ha-icon-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@ -409,6 +409,8 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
value: number | undefined,
|
value: number | undefined,
|
||||||
inverted: boolean | undefined
|
inverted: boolean | undefined
|
||||||
) {
|
) {
|
||||||
|
if (this.disabled) return nothing;
|
||||||
|
|
||||||
const limit = inverted ? this.max : this.min;
|
const limit = inverted ? this.max : this.min;
|
||||||
|
|
||||||
const path = svgArc({
|
const path = svgArc({
|
||||||
@ -437,7 +439,10 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
const targetCircleDashArray = this._strokeCircleDashArc(target);
|
const targetCircleDashArray = this._strokeCircleDashArc(target);
|
||||||
|
|
||||||
const currentCircleDashArray =
|
const currentCircleDashArray =
|
||||||
this.current != null && showActive
|
this.current != null &&
|
||||||
|
showActive &&
|
||||||
|
current <= this.max &&
|
||||||
|
current >= this.min
|
||||||
? this._strokeCircleDashArc(this.current)
|
? this._strokeCircleDashArc(this.current)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@ -474,17 +479,11 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
@keydown=${this._handleKeyDown}
|
@keydown=${this._handleKeyDown}
|
||||||
@keyup=${this._handleKeyUp}
|
@keyup=${this._handleKeyUp}
|
||||||
/>
|
/>
|
||||||
<path
|
|
||||||
class="target"
|
|
||||||
d=${path}
|
|
||||||
stroke-dasharray=${targetCircleDashArray[0]}
|
|
||||||
stroke-dashoffset=${targetCircleDashArray[1]}
|
|
||||||
/>
|
|
||||||
${
|
${
|
||||||
currentCircleDashArray
|
currentCircleDashArray
|
||||||
? svg`
|
? svg`
|
||||||
<path
|
<path
|
||||||
class="current"
|
class="current arc-current"
|
||||||
d=${path}
|
d=${path}
|
||||||
stroke-dasharray=${currentCircleDashArray[0]}
|
stroke-dasharray=${currentCircleDashArray[0]}
|
||||||
stroke-dashoffset=${currentCircleDashArray[1]}
|
stroke-dashoffset=${currentCircleDashArray[1]}
|
||||||
@ -492,6 +491,12 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
`
|
`
|
||||||
: nothing
|
: nothing
|
||||||
}
|
}
|
||||||
|
<path
|
||||||
|
class="target"
|
||||||
|
d=${path}
|
||||||
|
stroke-dasharray=${targetCircleDashArray[0]}
|
||||||
|
stroke-dashoffset=${targetCircleDashArray[1]}
|
||||||
|
/>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -633,8 +638,8 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
fill: none;
|
fill: none;
|
||||||
stroke-linecap: round;
|
stroke-linecap: round;
|
||||||
stroke-width: 8px;
|
stroke-width: 8px;
|
||||||
stroke: white;
|
stroke: var(--primary-text-color);
|
||||||
opacity: 0.6;
|
opacity: 0.5;
|
||||||
transition:
|
transition:
|
||||||
stroke-width 300ms ease-in-out,
|
stroke-width 300ms ease-in-out,
|
||||||
stroke-dasharray 300ms ease-in-out,
|
stroke-dasharray 300ms ease-in-out,
|
||||||
@ -643,6 +648,10 @@ export class HaControlCircularSlider extends LitElement {
|
|||||||
opacity 180ms ease-in-out;
|
opacity 180ms ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.arc-current {
|
||||||
|
stroke: var(--clear-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
.arc-clear {
|
.arc-clear {
|
||||||
stroke: var(--clear-background-color);
|
stroke: var(--clear-background-color);
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,14 @@
|
|||||||
|
import {
|
||||||
|
mdiClockOutline,
|
||||||
|
mdiFan,
|
||||||
|
mdiFire,
|
||||||
|
mdiHeatWave,
|
||||||
|
mdiPower,
|
||||||
|
mdiSnowflake,
|
||||||
|
mdiSunSnowflakeVariant,
|
||||||
|
mdiThermostatAuto,
|
||||||
|
mdiWaterPercent,
|
||||||
|
} from "@mdi/js";
|
||||||
import {
|
import {
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
HassEntityBase,
|
HassEntityBase,
|
||||||
@ -74,7 +85,7 @@ const hvacModeOrdering: { [key in HvacMode]: number } = {
|
|||||||
export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) =>
|
export const compareClimateHvacModes = (mode1: HvacMode, mode2: HvacMode) =>
|
||||||
hvacModeOrdering[mode1] - hvacModeOrdering[mode2];
|
hvacModeOrdering[mode1] - hvacModeOrdering[mode2];
|
||||||
|
|
||||||
export const HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
|
export const CLIMATE_HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
|
||||||
cooling: "cool",
|
cooling: "cool",
|
||||||
drying: "dry",
|
drying: "dry",
|
||||||
fan: "fan_only",
|
fan: "fan_only",
|
||||||
@ -83,3 +94,23 @@ export const HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
|
|||||||
idle: "off",
|
idle: "off",
|
||||||
off: "off",
|
off: "off",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const CLIMATE_HVAC_ACTION_ICONS: Record<HvacAction, string> = {
|
||||||
|
cooling: mdiSnowflake,
|
||||||
|
drying: mdiWaterPercent,
|
||||||
|
fan: mdiFan,
|
||||||
|
heating: mdiFire,
|
||||||
|
idle: mdiClockOutline,
|
||||||
|
off: mdiPower,
|
||||||
|
preheating: mdiHeatWave,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CLIMATE_HVAC_MODE_ICONS: Record<HvacMode, string> = {
|
||||||
|
cool: mdiSnowflake,
|
||||||
|
dry: mdiWaterPercent,
|
||||||
|
fan_only: mdiFan,
|
||||||
|
auto: mdiThermostatAuto,
|
||||||
|
heat: mdiFire,
|
||||||
|
off: mdiPower,
|
||||||
|
heat_cool: mdiSunSnowflakeVariant,
|
||||||
|
};
|
||||||
|
@ -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<Record<Target, number>> = {};
|
||||||
|
|
||||||
|
@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`
|
||||||
|
<p class="action">
|
||||||
|
${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")}
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`
|
||||||
|
<div class="buttons">
|
||||||
|
<ha-outlined-icon-button
|
||||||
|
style=${styleMap({
|
||||||
|
"--md-sys-color-outline": color,
|
||||||
|
})}
|
||||||
|
.target=${target}
|
||||||
|
.step=${-this._step}
|
||||||
|
@click=${this._handleButton}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiMinus}></ha-svg-icon>
|
||||||
|
</ha-outlined-icon-button>
|
||||||
|
<ha-outlined-icon-button
|
||||||
|
style=${styleMap({
|
||||||
|
"--md-sys-color-outline": color,
|
||||||
|
})}
|
||||||
|
.target=${target}
|
||||||
|
.step=${this._step}
|
||||||
|
@click=${this._handleButton}
|
||||||
|
>
|
||||||
|
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</ha-outlined-icon-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`
|
||||||
|
<p class="temperature">
|
||||||
|
<span aria-hidden="true">
|
||||||
|
${temperatureInteger}
|
||||||
|
${digits !== 0
|
||||||
|
? html`<span class="decimal">${temperatureDecimal}</span>`
|
||||||
|
: nothing}
|
||||||
|
<span class="unit">
|
||||||
|
${this.hass.config.unit_system.temperature}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span class="visually-hidden">
|
||||||
|
${this.stateObj.attributes.temperature}
|
||||||
|
${this.hass.config.unit_system.temperature}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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`
|
||||||
|
<div
|
||||||
|
class="container"
|
||||||
|
style=${styleMap({
|
||||||
|
"--main-color": mainColor,
|
||||||
|
"--action-color": actionColor,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ha-control-circular-slider
|
||||||
|
.inverted=${mode === "cool" || hasOnlyCoolMode}
|
||||||
|
.value=${this._targetTemperature.value}
|
||||||
|
.min=${this._min}
|
||||||
|
.max=${this._max}
|
||||||
|
.step=${this._step}
|
||||||
|
.current=${this.stateObj.attributes.current_temperature}
|
||||||
|
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||||
|
@value-changed=${this._valueChanged}
|
||||||
|
@value-changing=${this._valueChanging}
|
||||||
|
>
|
||||||
|
</ha-control-circular-slider>
|
||||||
|
<div class="info">
|
||||||
|
<div class="action-container">${this._renderHvacAction()}</div>
|
||||||
|
<div class="temperature-container">
|
||||||
|
${this._renderTargetTemperature(this._targetTemperature.value)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this._renderTemperatureButtons("value")}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
supportsTargetTemperatureRange &&
|
||||||
|
this._targetTemperature.low != null &&
|
||||||
|
this._targetTemperature.high != null
|
||||||
|
) {
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="container"
|
||||||
|
style=${styleMap({
|
||||||
|
"--low-color": lowColor,
|
||||||
|
"--high-color": highColor,
|
||||||
|
"--action-color": actionColor,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ha-control-circular-slider
|
||||||
|
dual
|
||||||
|
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||||
|
.low=${this._targetTemperature.low}
|
||||||
|
.high=${this._targetTemperature.high}
|
||||||
|
.min=${this._min}
|
||||||
|
.max=${this._max}
|
||||||
|
.step=${this._step}
|
||||||
|
.current=${this.stateObj.attributes.current_temperature}
|
||||||
|
@low-changed=${this._valueChanged}
|
||||||
|
@low-changing=${this._valueChanging}
|
||||||
|
@high-changed=${this._valueChanged}
|
||||||
|
@high-changing=${this._valueChanging}
|
||||||
|
>
|
||||||
|
</ha-control-circular-slider>
|
||||||
|
<div class="info">
|
||||||
|
<div class="action-container">${this._renderHvacAction()}</div>
|
||||||
|
<div class="temperature-container dual">
|
||||||
|
<button
|
||||||
|
@click=${this._handleSelectTemp}
|
||||||
|
.target=${"low"}
|
||||||
|
class=${classMap({
|
||||||
|
selected: this._selectTargetTemperature === "low",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${this._renderTargetTemperature(this._targetTemperature.low)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click=${this._handleSelectTemp}
|
||||||
|
.target=${"high"}
|
||||||
|
class=${classMap({
|
||||||
|
selected: this._selectTargetTemperature === "high",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${this._renderTargetTemperature(this._targetTemperature.high)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${this._renderTemperatureButtons(this._selectTargetTemperature, true)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div
|
||||||
|
class="container"
|
||||||
|
style=${styleMap({
|
||||||
|
"--action-color": actionColor,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<ha-control-circular-slider
|
||||||
|
.current=${this.stateObj.attributes.current_temperature}
|
||||||
|
.min=${this._min}
|
||||||
|
.max=${this._max}
|
||||||
|
.step=${this._step}
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
</ha-control-circular-slider>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -19,6 +19,7 @@ export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
|
|||||||
export const DOMAINS_WITH_NEW_MORE_INFO = [
|
export const DOMAINS_WITH_NEW_MORE_INFO = [
|
||||||
"alarm_control_panel",
|
"alarm_control_panel",
|
||||||
"cover",
|
"cover",
|
||||||
|
"climate",
|
||||||
"fan",
|
"fan",
|
||||||
"input_boolean",
|
"input_boolean",
|
||||||
"light",
|
"light",
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import {
|
import {
|
||||||
css,
|
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
|
css,
|
||||||
|
html,
|
||||||
nothing,
|
nothing,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { property } from "lit/decorators";
|
import { property } from "lit/decorators";
|
||||||
@ -17,8 +17,9 @@ import {
|
|||||||
} from "../../../common/entity/compute_attribute_display";
|
} from "../../../common/entity/compute_attribute_display";
|
||||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
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 { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||||
import "../../../components/ha-climate-control";
|
|
||||||
import "../../../components/ha-select";
|
import "../../../components/ha-select";
|
||||||
import "../../../components/ha-slider";
|
import "../../../components/ha-slider";
|
||||||
import "../../../components/ha-switch";
|
import "../../../components/ha-switch";
|
||||||
@ -28,6 +29,8 @@ import {
|
|||||||
compareClimateHvacModes,
|
compareClimateHvacModes,
|
||||||
} from "../../../data/climate";
|
} from "../../../data/climate";
|
||||||
import { HomeAssistant } from "../../../types";
|
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 {
|
class MoreInfoClimate extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -73,13 +76,60 @@ class MoreInfoClimate extends LitElement {
|
|||||||
ClimateEntityFeature.AUX_HEAT
|
ClimateEntityFeature.AUX_HEAT
|
||||||
);
|
);
|
||||||
|
|
||||||
const temperatureStepSize =
|
const currentTemperature = this.stateObj.attributes.current_temperature;
|
||||||
stateObj.attributes.target_temp_step ||
|
const currentHumidity = this.stateObj.attributes.current_humidity;
|
||||||
(hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1);
|
|
||||||
|
|
||||||
const rtlDirection = computeRTLDirection(hass);
|
const rtlDirection = computeRTLDirection(hass);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
${currentTemperature || currentHumidity
|
||||||
|
? html`<div class="current">
|
||||||
|
${currentTemperature != null
|
||||||
|
? html`
|
||||||
|
<div>
|
||||||
|
<p class="label">
|
||||||
|
${computeAttributeNameDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.entities,
|
||||||
|
"current_temperature"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p class="value">
|
||||||
|
${formatNumber(currentTemperature, this.hass.locale)}
|
||||||
|
${this.hass.config.unit_system.temperature}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
${currentHumidity != null
|
||||||
|
? html`
|
||||||
|
<div>
|
||||||
|
<p class="label">
|
||||||
|
${computeAttributeNameDisplay(
|
||||||
|
this.hass.localize,
|
||||||
|
this.stateObj,
|
||||||
|
this.hass.entities,
|
||||||
|
"current_humidity"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p class="value">
|
||||||
|
${formatNumber(
|
||||||
|
currentHumidity,
|
||||||
|
this.hass.locale
|
||||||
|
)}${blankBeforePercent(this.hass.locale)}%
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>`
|
||||||
|
: nothing}
|
||||||
|
<div class="controls">
|
||||||
|
<ha-more-info-climate-temperature
|
||||||
|
.hass=${this.hass}
|
||||||
|
.stateObj=${this.stateObj}
|
||||||
|
></ha-more-info-climate-temperature>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
"has-current_temperature":
|
"has-current_temperature":
|
||||||
@ -94,64 +144,6 @@ class MoreInfoClimate extends LitElement {
|
|||||||
"has-preset_mode": supportPresetMode,
|
"has-preset_mode": supportPresetMode,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div class="container-temperature">
|
|
||||||
<div class=${stateObj.state}>
|
|
||||||
${supportTargetTemperature || supportTargetTemperatureRange
|
|
||||||
? html`
|
|
||||||
<div>
|
|
||||||
${computeAttributeNameDisplay(
|
|
||||||
hass.localize,
|
|
||||||
stateObj,
|
|
||||||
hass.entities,
|
|
||||||
"temperature"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${stateObj.attributes.temperature !== undefined &&
|
|
||||||
stateObj.attributes.temperature !== null
|
|
||||||
? html`
|
|
||||||
<ha-climate-control
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${stateObj.attributes.temperature}
|
|
||||||
.unit=${hass.config.unit_system.temperature}
|
|
||||||
.step=${temperatureStepSize}
|
|
||||||
.min=${stateObj.attributes.min_temp}
|
|
||||||
.max=${stateObj.attributes.max_temp}
|
|
||||||
@change=${this._targetTemperatureChanged}
|
|
||||||
></ha-climate-control>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${(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`
|
|
||||||
<ha-climate-control
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${stateObj.attributes.target_temp_low}
|
|
||||||
.unit=${hass.config.unit_system.temperature}
|
|
||||||
.step=${temperatureStepSize}
|
|
||||||
.min=${stateObj.attributes.min_temp}
|
|
||||||
.max=${stateObj.attributes.target_temp_high}
|
|
||||||
class="range-control-left"
|
|
||||||
@change=${this._targetTemperatureLowChanged}
|
|
||||||
></ha-climate-control>
|
|
||||||
<ha-climate-control
|
|
||||||
.hass=${this.hass}
|
|
||||||
.value=${stateObj.attributes.target_temp_high}
|
|
||||||
.unit=${hass.config.unit_system.temperature}
|
|
||||||
.step=${temperatureStepSize}
|
|
||||||
.min=${stateObj.attributes.target_temp_low}
|
|
||||||
.max=${stateObj.attributes.max_temp}
|
|
||||||
class="range-control-right"
|
|
||||||
@change=${this._targetTemperatureHighChanged}
|
|
||||||
></ha-climate-control>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
${supportTargetHumidity
|
${supportTargetHumidity
|
||||||
? html`
|
? html`
|
||||||
<div class="container-humidity">
|
<div class="container-humidity">
|
||||||
@ -184,34 +176,32 @@ class MoreInfoClimate extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
<div class="container-hvac_modes">
|
<div class="container-hvac_modes">
|
||||||
<div class="controls">
|
<ha-select
|
||||||
<ha-select
|
.label=${hass.localize("ui.card.climate.operation")}
|
||||||
.label=${hass.localize("ui.card.climate.operation")}
|
.value=${stateObj.state}
|
||||||
.value=${stateObj.state}
|
fixedMenuPosition
|
||||||
fixedMenuPosition
|
naturalMenuWidth
|
||||||
naturalMenuWidth
|
@selected=${this._handleOperationmodeChanged}
|
||||||
@selected=${this._handleOperationmodeChanged}
|
@closed=${stopPropagation}
|
||||||
@closed=${stopPropagation}
|
>
|
||||||
>
|
${stateObj.attributes.hvac_modes
|
||||||
${stateObj.attributes.hvac_modes
|
.concat()
|
||||||
.concat()
|
.sort(compareClimateHvacModes)
|
||||||
.sort(compareClimateHvacModes)
|
.map(
|
||||||
.map(
|
(mode) => html`
|
||||||
(mode) => html`
|
<mwc-list-item .value=${mode}>
|
||||||
<mwc-list-item .value=${mode}>
|
${computeStateDisplay(
|
||||||
${computeStateDisplay(
|
hass.localize,
|
||||||
hass.localize,
|
stateObj,
|
||||||
stateObj,
|
hass.locale,
|
||||||
hass.locale,
|
this.hass.config,
|
||||||
this.hass.config,
|
hass.entities,
|
||||||
hass.entities,
|
mode
|
||||||
mode
|
)}
|
||||||
)}
|
</mwc-list-item>
|
||||||
</mwc-list-item>
|
`
|
||||||
`
|
)}
|
||||||
)}
|
</ha-select>
|
||||||
</ha-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${supportPresetMode && stateObj.attributes.preset_modes
|
${supportPresetMode && stateObj.attributes.preset_modes
|
||||||
@ -358,42 +348,6 @@ class MoreInfoClimate extends LitElement {
|
|||||||
}, 500);
|
}, 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) {
|
private _targetHumiditySliderChanged(ev) {
|
||||||
const newVal = ev.target.value;
|
const newVal = ev.target.value;
|
||||||
this._callServiceHelper(
|
this._callServiceHelper(
|
||||||
@ -492,48 +446,76 @@ class MoreInfoClimate extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
return css`
|
return [
|
||||||
:host {
|
moreInfoControlStyle,
|
||||||
color: var(--primary-text-color);
|
css`
|
||||||
}
|
:host {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
ha-select {
|
.current {
|
||||||
width: 100%;
|
display: flex;
|
||||||
margin-top: 8px;
|
flex-direction: row;
|
||||||
}
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
ha-slider {
|
.current div {
|
||||||
width: 100%;
|
display: flex;
|
||||||
}
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.container-humidity .single-row {
|
.current p {
|
||||||
display: flex;
|
margin: 0;
|
||||||
height: 50px;
|
text-align: center;
|
||||||
}
|
color: var(--primary-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
.target-humidity {
|
.current .label {
|
||||||
width: 90px;
|
opacity: 0.8;
|
||||||
font-size: 200%;
|
font-size: 14px;
|
||||||
margin: auto;
|
line-height: 16px;
|
||||||
direction: ltr;
|
letter-spacing: 0.4px;
|
||||||
}
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
ha-climate-control.range-control-left,
|
.current .value {
|
||||||
ha-climate-control.range-control-right {
|
font-size: 22px;
|
||||||
float: left;
|
font-weight: 500;
|
||||||
width: 46%;
|
line-height: 28px;
|
||||||
}
|
}
|
||||||
ha-climate-control.range-control-left {
|
ha-select {
|
||||||
margin-right: 4%;
|
width: 100%;
|
||||||
}
|
margin-top: 8px;
|
||||||
ha-climate-control.range-control-right {
|
}
|
||||||
margin-left: 4%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.single-row {
|
ha-slider {
|
||||||
padding: 8px 0;
|
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;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ import {
|
|||||||
HassEntity,
|
HassEntity,
|
||||||
HassEntityAttributeBase,
|
HassEntityAttributeBase,
|
||||||
} from "home-assistant-js-websocket";
|
} from "home-assistant-js-websocket";
|
||||||
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
|
import { ClimateEntityFeature } from "../data/climate";
|
||||||
|
|
||||||
const now = () => new Date().toISOString();
|
const now = () => new Date().toISOString();
|
||||||
const randomTime = () =>
|
const randomTime = () =>
|
||||||
@ -313,6 +315,45 @@ class ClimateEntity extends Entity {
|
|||||||
super.handleService(domain, service, data);
|
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 {
|
class GroupEntity extends Entity {
|
||||||
|
@ -34,7 +34,7 @@ import { isValidEntityId } from "../../../common/entity/valid_entity_id";
|
|||||||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||||
import { LocalizeFunc } from "../../../common/translations/localize";
|
import { LocalizeFunc } from "../../../common/translations/localize";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import { HVAC_ACTION_TO_MODE } from "../../../data/climate";
|
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate";
|
||||||
import {
|
import {
|
||||||
configContext,
|
configContext,
|
||||||
entitiesContext,
|
entitiesContext,
|
||||||
@ -343,8 +343,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
|
|||||||
}
|
}
|
||||||
if (stateObj.attributes.hvac_action) {
|
if (stateObj.attributes.hvac_action) {
|
||||||
const hvacAction = 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) {
|
||||||
return stateColorCss(stateObj, HVAC_ACTION_TO_MODE[hvacAction]);
|
return stateColorCss(stateObj, CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -29,7 +29,7 @@ import {
|
|||||||
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
import { iconColorCSS } from "../../../common/style/icon_color_css";
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon";
|
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 { isUnavailableState } from "../../../data/entity";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { computeCardSize } from "../common/compute-card-size";
|
import { computeCardSize } from "../common/compute-card-size";
|
||||||
@ -201,8 +201,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
|||||||
private _computeColor(stateObj: HassEntity): string | undefined {
|
private _computeColor(stateObj: HassEntity): string | undefined {
|
||||||
if (stateObj.attributes.hvac_action) {
|
if (stateObj.attributes.hvac_action) {
|
||||||
const hvacAction = 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) {
|
||||||
return stateColorCss(stateObj, HVAC_ACTION_TO_MODE[hvacAction]);
|
return stateColorCss(stateObj, CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]);
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,7 @@ import { createEntityNotFoundWarning } from "../components/hui-warning";
|
|||||||
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
import { LovelaceCard, LovelaceCardEditor } from "../types";
|
||||||
import { ThermostatCardConfig } from "./types";
|
import { ThermostatCardConfig } from "./types";
|
||||||
|
|
||||||
|
// TODO: Need to align these icon to more info icons
|
||||||
const modeIcons: { [mode in HvacMode]: string } = {
|
const modeIcons: { [mode in HvacMode]: string } = {
|
||||||
auto: mdiCalendarSync,
|
auto: mdiCalendarSync,
|
||||||
heat_cool: mdiAutorenew,
|
heat_cool: mdiAutorenew,
|
||||||
|
@ -1,40 +1,11 @@
|
|||||||
import {
|
|
||||||
mdiClockOutline,
|
|
||||||
mdiFan,
|
|
||||||
mdiFire,
|
|
||||||
mdiHeatWave,
|
|
||||||
mdiPower,
|
|
||||||
mdiSnowflake,
|
|
||||||
mdiWaterPercent,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import { stateColorCss } from "../../../../../common/entity/state_color";
|
import { stateColorCss } from "../../../../../common/entity/state_color";
|
||||||
import {
|
import {
|
||||||
|
CLIMATE_HVAC_ACTION_ICONS,
|
||||||
|
CLIMATE_HVAC_ACTION_TO_MODE,
|
||||||
ClimateEntity,
|
ClimateEntity,
|
||||||
HvacAction,
|
|
||||||
HvacMode,
|
|
||||||
} from "../../../../../data/climate";
|
} from "../../../../../data/climate";
|
||||||
import { ComputeBadgeFunction } from "./tile-badge";
|
import { ComputeBadgeFunction } from "./tile-badge";
|
||||||
|
|
||||||
export const CLIMATE_HVAC_ACTION_ICONS: Record<HvacAction, string> = {
|
|
||||||
cooling: mdiSnowflake,
|
|
||||||
drying: mdiWaterPercent,
|
|
||||||
fan: mdiFan,
|
|
||||||
heating: mdiFire,
|
|
||||||
idle: mdiClockOutline,
|
|
||||||
off: mdiPower,
|
|
||||||
preheating: mdiHeatWave,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CLIMATE_HVAC_ACTION_MODE: Record<HvacAction, HvacMode> = {
|
|
||||||
cooling: "cool",
|
|
||||||
drying: "dry",
|
|
||||||
fan: "fan_only",
|
|
||||||
heating: "heat",
|
|
||||||
idle: "off",
|
|
||||||
off: "off",
|
|
||||||
preheating: "heat",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {
|
export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {
|
||||||
const hvacAction = (stateObj as ClimateEntity).attributes.hvac_action;
|
const hvacAction = (stateObj as ClimateEntity).attributes.hvac_action;
|
||||||
|
|
||||||
@ -44,6 +15,6 @@ export const computeClimateBadge: ComputeBadgeFunction = (stateObj) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
iconPath: CLIMATE_HVAC_ACTION_ICONS[hvacAction],
|
iconPath: CLIMATE_HVAC_ACTION_ICONS[hvacAction],
|
||||||
color: stateColorCss(stateObj, CLIMATE_HVAC_ACTION_MODE[hvacAction]),
|
color: stateColorCss(stateObj, CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -109,7 +109,7 @@ documentContainer.innerHTML = `<custom-style>
|
|||||||
--yellow-color: #ffeb3b;
|
--yellow-color: #ffeb3b;
|
||||||
--amber-color: #ffc107;
|
--amber-color: #ffc107;
|
||||||
--orange-color: #ff9800;
|
--orange-color: #ff9800;
|
||||||
--deep-orange-color: #ff5722;
|
--deep-orange-color: #ff6f22;
|
||||||
--brown-color: #795548;
|
--brown-color: #795548;
|
||||||
--light-grey-color: #bdbdbd;
|
--light-grey-color: #bdbdbd;
|
||||||
--grey-color: #9e9e9e;
|
--grey-color: #9e9e9e;
|
||||||
|
@ -996,6 +996,10 @@
|
|||||||
"open": "Open",
|
"open": "Open",
|
||||||
"lock": "Lock",
|
"lock": "Lock",
|
||||||
"unlock": "Unlock"
|
"unlock": "Unlock"
|
||||||
|
},
|
||||||
|
"climate": {
|
||||||
|
"target_label": "{action} to target",
|
||||||
|
"target": "Target"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity_registry": {
|
"entity_registry": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user