mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add target temperature tile feature for climate and water heater (#17697)
This commit is contained in:
parent
6f99a39b55
commit
7040c6d469
@ -0,0 +1,3 @@
|
||||
---
|
||||
title: Control Number Buttons
|
||||
---
|
100
gallery/src/pages/components/ha-control-number-buttons.ts
Normal file
100
gallery/src/pages/components/ha-control-number-buttons.ts
Normal file
@ -0,0 +1,100 @@
|
||||
import { LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import "../../../../src/components/ha-card";
|
||||
import "../../../../src/components/ha-control-number-buttons";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
|
||||
const buttons: {
|
||||
id: string;
|
||||
label: string;
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
class?: string;
|
||||
}[] = [
|
||||
{
|
||||
id: "basic",
|
||||
label: "Basic",
|
||||
},
|
||||
{
|
||||
id: "min_max_step",
|
||||
label: "With min/max and step",
|
||||
min: 5,
|
||||
max: 25,
|
||||
step: 0.5,
|
||||
},
|
||||
{
|
||||
id: "custom",
|
||||
label: "Custom",
|
||||
class: "custom",
|
||||
},
|
||||
];
|
||||
|
||||
@customElement("demo-components-ha-control-number-buttons")
|
||||
export class DemoHarControlNumberButtons extends LitElement {
|
||||
@state() value = 5;
|
||||
|
||||
private _valueChanged(ev) {
|
||||
this.value = ev.detail.value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${repeat(buttons, (button) => {
|
||||
const { id, label, ...config } = button;
|
||||
return html`
|
||||
<ha-card>
|
||||
<div class="card-content">
|
||||
<label id=${id}>${label}</label>
|
||||
<pre>Config: ${JSON.stringify(config)}</pre>
|
||||
<ha-control-number-buttons
|
||||
.value=${this.value}
|
||||
.min=${config.min}
|
||||
.max=${config.max}
|
||||
.step=${config.step}
|
||||
class=${ifDefined(config.class)}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${label}
|
||||
>
|
||||
</ha-control-number-buttons>
|
||||
</div>
|
||||
</ha-card>
|
||||
`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-card {
|
||||
max-width: 600px;
|
||||
margin: 24px auto;
|
||||
}
|
||||
pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
label {
|
||||
font-weight: 600;
|
||||
}
|
||||
.custom {
|
||||
color: #2196f3;
|
||||
--control-number-buttons-color: #2196f3;
|
||||
--control-number-buttons-background-color: #2196f3;
|
||||
--control-number-buttons-background-opacity: 0.1;
|
||||
--control-number-buttons-thickness: 100px;
|
||||
--control-number-buttons-border-radius: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"demo-components-ha-control-number-buttons": DemoHarControlNumberButtons;
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ export const clamp = (value: number, min: number, max: number) =>
|
||||
// Variant that only applies the clamping to a border if the border is defined
|
||||
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
||||
let result: number;
|
||||
result = min ? Math.max(value, min) : value;
|
||||
result = max ? Math.min(result, max) : result;
|
||||
result = min != null ? Math.max(value, min) : value;
|
||||
result = max != null ? Math.min(result, max) : result;
|
||||
return result;
|
||||
};
|
||||
|
258
src/components/ha-control-number-buttons.ts
Normal file
258
src/components/ha-control-number-buttons.ts
Normal file
@ -0,0 +1,258 @@
|
||||
import { mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { conditionalClamp } from "../common/number/clamp";
|
||||
import { formatNumber } from "../common/number/format_number";
|
||||
import { FrontendLocaleData } from "../data/translation";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
|
||||
const A11Y_KEY_CODES = new Set([
|
||||
"ArrowRight",
|
||||
"ArrowUp",
|
||||
"ArrowLeft",
|
||||
"ArrowDown",
|
||||
"PageUp",
|
||||
"PageDown",
|
||||
"Home",
|
||||
"End",
|
||||
]);
|
||||
|
||||
@customElement("ha-control-number-buttons")
|
||||
export class HaControlNumberButton extends LitElement {
|
||||
@property({ attribute: false }) public locale?: FrontendLocaleData;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) disabled = false;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Number }) public step?: number;
|
||||
|
||||
@property({ type: Number }) public value?: number;
|
||||
|
||||
@property({ type: Number }) public min?: number;
|
||||
|
||||
@property({ type: Number }) public max?: number;
|
||||
|
||||
@property({ attribute: "false" })
|
||||
public formatOptions: Intl.NumberFormatOptions = {};
|
||||
|
||||
@query("#input") _input!: HTMLDivElement;
|
||||
|
||||
private boundedValue(value: number) {
|
||||
const clamped = conditionalClamp(value, this.min, this.max);
|
||||
return Math.round(clamped / this._step) * this._step;
|
||||
}
|
||||
|
||||
private get _step() {
|
||||
return this.step ?? 1;
|
||||
}
|
||||
|
||||
private get _value() {
|
||||
return this.value ?? 0;
|
||||
}
|
||||
|
||||
private get _tenPercentStep() {
|
||||
if (this.max == null || this.min == null) return this._step;
|
||||
const range = this.max - this.min / 10;
|
||||
|
||||
if (range <= this._step) return this._step;
|
||||
return Math.max(range / 10);
|
||||
}
|
||||
|
||||
private _handlePlusButton() {
|
||||
this._increment();
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
this._input.focus();
|
||||
}
|
||||
|
||||
private _handleMinusButton() {
|
||||
this._decrement();
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
this._input.focus();
|
||||
}
|
||||
|
||||
private _increment() {
|
||||
this.value = this.boundedValue(this._value + this._step);
|
||||
}
|
||||
|
||||
private _decrement() {
|
||||
this.value = this.boundedValue(this._value - this._step);
|
||||
}
|
||||
|
||||
_handleKeyDown(e: KeyboardEvent) {
|
||||
if (!A11Y_KEY_CODES.has(e.code)) return;
|
||||
e.preventDefault();
|
||||
switch (e.code) {
|
||||
case "ArrowRight":
|
||||
case "ArrowUp":
|
||||
this._increment();
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
case "ArrowDown":
|
||||
this._decrement();
|
||||
break;
|
||||
case "PageUp":
|
||||
this.value = this.boundedValue(this._value + this._tenPercentStep);
|
||||
break;
|
||||
case "PageDown":
|
||||
this.value = this.boundedValue(this._value - this._tenPercentStep);
|
||||
break;
|
||||
case "Home":
|
||||
if (this.min != null) {
|
||||
this.value = this.min;
|
||||
}
|
||||
break;
|
||||
case "End":
|
||||
if (this.max != null) {
|
||||
this.value = this.max;
|
||||
}
|
||||
break;
|
||||
}
|
||||
fireEvent(this, "value-changed", { value: this.value });
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const displayedValue =
|
||||
this.value != null
|
||||
? formatNumber(this.value, this.locale, this.formatOptions)
|
||||
: "-";
|
||||
|
||||
return html`
|
||||
<div class="container">
|
||||
<div
|
||||
id="input"
|
||||
class="value"
|
||||
role="number-button"
|
||||
tabindex="0"
|
||||
aria-valuenow=${this.value}
|
||||
aria-valuemin=${this.min}
|
||||
aria-valuemax=${this.max}
|
||||
aria-label=${ifDefined(this.label)}
|
||||
.disabled=${this.disabled}
|
||||
@keydown=${this._handleKeyDown}
|
||||
>
|
||||
${displayedValue}
|
||||
</div>
|
||||
<button
|
||||
class="button minus"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
aria-label="decrement"
|
||||
@click=${this._handleMinusButton}
|
||||
.disabled=${this.disabled ||
|
||||
(this.min != null && this._value <= this.min)}
|
||||
>
|
||||
<ha-svg-icon aria-hidden .path=${mdiMinus}></ha-svg-icon>
|
||||
</button>
|
||||
<button
|
||||
class="button plus"
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
aria-label="increment"
|
||||
@click=${this._handlePlusButton}
|
||||
.disabled=${this.disabled ||
|
||||
(this.max != null && this._value >= this.max)}
|
||||
>
|
||||
<ha-svg-icon aria-hidden .path=${mdiPlus}></ha-svg-icon>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
--control-number-buttons-focus-color: var(--primary-color);
|
||||
--control-number-buttons-background-color: var(--disabled-color);
|
||||
--control-number-buttons-background-opacity: 0.2;
|
||||
--control-number-buttons-border-radius: 10px;
|
||||
--mdc-icon-size: 16px;
|
||||
height: 40px;
|
||||
width: 200px;
|
||||
color: var(--primary-text-color);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
transition: color 180ms ease-in-out;
|
||||
}
|
||||
:host([disabled]) {
|
||||
color: var(--disabled-color);
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 44px;
|
||||
border-radius: var(--control-number-buttons-border-radius);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-sizing: border-box;
|
||||
line-height: 0;
|
||||
overflow: hidden;
|
||||
/* For safari border-radius overflow */
|
||||
z-index: 0;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
outline: none;
|
||||
}
|
||||
.value::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: var(--control-number-buttons-background-color);
|
||||
transition:
|
||||
background-color 180ms ease-in-out,
|
||||
opacity 180ms ease-in-out;
|
||||
opacity: var(--control-number-buttons-background-opacity);
|
||||
}
|
||||
.value:focus-visible {
|
||||
box-shadow: 0 0 0 2px var(--control-number-buttons-focus-color);
|
||||
}
|
||||
.button {
|
||||
color: inherit;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
padding: 0;
|
||||
width: 35px;
|
||||
height: 40px;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
.button[disabled] {
|
||||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
}
|
||||
.button.minus {
|
||||
left: 0;
|
||||
}
|
||||
.button.plus {
|
||||
right: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-control-number-buttons": HaControlNumberButton;
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import {
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { UNIT_F } from "../../../../common/const";
|
||||
import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
|
||||
import { stateActive } from "../../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
@ -67,7 +68,7 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
||||
private get _step() {
|
||||
return (
|
||||
this.stateObj.attributes.target_temp_step ||
|
||||
(this.hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1)
|
||||
(this.hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { UNIT_F } from "../../../../common/const";
|
||||
import { stateActive } from "../../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
@ -44,7 +45,7 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
|
||||
private get _step() {
|
||||
return (
|
||||
this.stateObj.attributes.target_temp_step ||
|
||||
(this.hass.config.unit_system.temperature.indexOf("F") === -1 ? 0.5 : 1)
|
||||
(this.hass.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,14 +1,15 @@
|
||||
import "../tile-features/hui-alarm-modes-tile-feature";
|
||||
import "../tile-features/hui-climate-hvac-modes-tile-feature";
|
||||
import "../tile-features/hui-target-temperature-tile-feature";
|
||||
import "../tile-features/hui-cover-open-close-tile-feature";
|
||||
import "../tile-features/hui-cover-position-tile-feature";
|
||||
import "../tile-features/hui-cover-tilt-position-tile-feature";
|
||||
import "../tile-features/hui-cover-tilt-tile-feature";
|
||||
import "../tile-features/hui-fan-speed-tile-feature";
|
||||
import "../tile-features/hui-lawn-mower-commands-tile-feature";
|
||||
import "../tile-features/hui-light-brightness-tile-feature";
|
||||
import "../tile-features/hui-light-color-temp-tile-feature";
|
||||
import "../tile-features/hui-vacuum-commands-tile-feature";
|
||||
import "../tile-features/hui-lawn-mower-commands-tile-feature";
|
||||
import "../tile-features/hui-water-heater-operation-modes-tile-feature";
|
||||
import { LovelaceTileFeatureConfig } from "../tile-features/types";
|
||||
import {
|
||||
@ -17,17 +18,18 @@ import {
|
||||
} from "./create-element-base";
|
||||
|
||||
const TYPES: Set<LovelaceTileFeatureConfig["type"]> = new Set([
|
||||
"cover-open-close",
|
||||
"cover-position",
|
||||
"cover-tilt",
|
||||
"cover-tilt-position",
|
||||
"light-brightness",
|
||||
"light-color-temp",
|
||||
"vacuum-commands",
|
||||
"lawn-mower-commands",
|
||||
"fan-speed",
|
||||
"alarm-modes",
|
||||
"climate-hvac-modes",
|
||||
"cover-open-close",
|
||||
"cover-position",
|
||||
"cover-tilt-position",
|
||||
"cover-tilt",
|
||||
"fan-speed",
|
||||
"lawn-mower-commands",
|
||||
"light-brightness",
|
||||
"light-color-temp",
|
||||
"target-temperature",
|
||||
"vacuum-commands",
|
||||
"water-heater-operation-modes",
|
||||
]);
|
||||
|
||||
|
@ -37,6 +37,7 @@ import { supportsLightColorTempTileFeature } from "../../tile-features/hui-light
|
||||
import { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature";
|
||||
import { supportsWaterHeaterOperationModesTileFeature } from "../../tile-features/hui-water-heater-operation-modes-tile-feature";
|
||||
import { LovelaceTileFeatureConfig } from "../../tile-features/types";
|
||||
import { supportsTargetTemperatureTileFeature } from "../../tile-features/hui-target-temperature-tile-feature";
|
||||
|
||||
type FeatureType = LovelaceTileFeatureConfig["type"];
|
||||
type SupportsFeature = (stateObj: HassEntity) => boolean;
|
||||
@ -44,6 +45,7 @@ type SupportsFeature = (stateObj: HassEntity) => boolean;
|
||||
const FEATURE_TYPES: FeatureType[] = [
|
||||
"alarm-modes",
|
||||
"climate-hvac-modes",
|
||||
"target-temperature",
|
||||
"cover-open-close",
|
||||
"cover-position",
|
||||
"cover-tilt-position",
|
||||
@ -76,6 +78,7 @@ const SUPPORTS_FEATURE_TYPES: Record<FeatureType, SupportsFeature | undefined> =
|
||||
"lawn-mower-commands": supportsLawnMowerCommandTileFeature,
|
||||
"light-brightness": supportsLightBrightnessTileFeature,
|
||||
"light-color-temp": supportsLightColorTempTileFeature,
|
||||
"target-temperature": supportsTargetTemperatureTileFeature,
|
||||
"vacuum-commands": supportsVacuumCommandTileFeature,
|
||||
"water-heater-operation-modes":
|
||||
supportsWaterHeaterOperationModesTileFeature,
|
||||
@ -151,8 +154,10 @@ export class HuiTileCardFeaturesEditor extends LitElement {
|
||||
const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType];
|
||||
return customFeatureEntry?.name || type;
|
||||
}
|
||||
return this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.tile.features.types.${type}.label`
|
||||
return (
|
||||
this.hass!.localize(
|
||||
`ui.panel.lovelace.editor.card.tile.features.types.${type}.label`
|
||||
) || type
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,254 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement, nothing, PropertyValues } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { UNIT_F } from "../../../common/const";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import "../../../components/ha-control-button-group";
|
||||
import "../../../components/ha-control-number-buttons";
|
||||
import { ClimateEntity, ClimateEntityFeature } from "../../../data/climate";
|
||||
import { UNAVAILABLE } from "../../../data/entity";
|
||||
import {
|
||||
WaterHeaterEntity,
|
||||
WaterHeaterEntityFeature,
|
||||
} from "../../../data/water_heater";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
import { LovelaceTileFeature } from "../types";
|
||||
import { TargetTemperatureTileFeatureConfig } from "./types";
|
||||
|
||||
type Target = "value" | "low" | "high";
|
||||
|
||||
export const supportsTargetTemperatureTileFeature = (stateObj: HassEntity) => {
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
(domain === "climate" &&
|
||||
(supportsFeature(stateObj, ClimateEntityFeature.TARGET_TEMPERATURE) ||
|
||||
supportsFeature(
|
||||
stateObj,
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
))) ||
|
||||
(domain === "water_heater" &&
|
||||
supportsFeature(stateObj, WaterHeaterEntityFeature.TARGET_TEMPERATURE))
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-target-temperature-tile-feature")
|
||||
class HuiTargetTemperatureTileFeature
|
||||
extends LitElement
|
||||
implements LovelaceTileFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj?:
|
||||
| ClimateEntity
|
||||
| WaterHeaterEntity;
|
||||
|
||||
@state() private _config?: TargetTemperatureTileFeatureConfig;
|
||||
|
||||
@state() private _targetTemperature: Partial<Record<Target, number>> = {};
|
||||
|
||||
static getStubConfig(): TargetTemperatureTileFeatureConfig {
|
||||
return {
|
||||
type: "target-temperature",
|
||||
};
|
||||
}
|
||||
|
||||
public setConfig(config: TargetTemperatureTileFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
super.willUpdate(changedProp);
|
||||
if (changedProp.has("stateObj")) {
|
||||
this._targetTemperature = {
|
||||
value: this.stateObj!.attributes.temperature,
|
||||
low:
|
||||
"target_temp_low" in this.stateObj!.attributes
|
||||
? this.stateObj!.attributes.target_temp_low
|
||||
: undefined,
|
||||
high:
|
||||
"target_temp_high" in this.stateObj!.attributes
|
||||
? this.stateObj!.attributes.target_temp_high
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private get _step() {
|
||||
return (
|
||||
this.stateObj!.attributes.target_temp_step ||
|
||||
(this.hass!.config.unit_system.temperature === UNIT_F ? 1 : 0.5)
|
||||
);
|
||||
}
|
||||
|
||||
private get _min() {
|
||||
return this.stateObj!.attributes.min_temp;
|
||||
}
|
||||
|
||||
private get _max() {
|
||||
return this.stateObj!.attributes.max_temp;
|
||||
}
|
||||
|
||||
private async _valueChanged(ev: CustomEvent) {
|
||||
const value = (ev.detail as any).value;
|
||||
if (isNaN(value)) return;
|
||||
const target = (ev.currentTarget as any).target ?? "value";
|
||||
|
||||
this._targetTemperature = {
|
||||
...this._targetTemperature,
|
||||
[target]: value,
|
||||
};
|
||||
this._debouncedCallService(target);
|
||||
}
|
||||
|
||||
private _debouncedCallService = debounce(
|
||||
(target: Target) => this._callService(target),
|
||||
1000
|
||||
);
|
||||
|
||||
private _callService(type: string) {
|
||||
const domain = computeStateDomain(this.stateObj!);
|
||||
if (type === "high" || type === "low") {
|
||||
this.hass!.callService(domain, "set_temperature", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
target_temp_low: this._targetTemperature.low,
|
||||
target_temp_high: this._targetTemperature.high,
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.hass!.callService(domain, "set_temperature", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
temperature: this._targetTemperature.value,
|
||||
});
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.stateObj ||
|
||||
!supportsTargetTemperatureTileFeature(this.stateObj)
|
||||
) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const stateColor = stateColorCss(this.stateObj);
|
||||
const digits = this._step.toString().split(".")?.[1]?.length ?? 0;
|
||||
|
||||
const options = {
|
||||
maximumFractionDigits: digits,
|
||||
minimumFractionDigits: digits,
|
||||
};
|
||||
|
||||
const domain = computeStateDomain(this.stateObj!);
|
||||
|
||||
if (
|
||||
(domain === "climate" &&
|
||||
supportsFeature(
|
||||
this.stateObj,
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE
|
||||
)) ||
|
||||
(domain === "water_heater" &&
|
||||
supportsFeature(
|
||||
this.stateObj,
|
||||
WaterHeaterEntityFeature.TARGET_TEMPERATURE
|
||||
))
|
||||
) {
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
<ha-control-number-buttons
|
||||
.formatOptions=${options}
|
||||
.target="value"
|
||||
.value=${this.stateObj.attributes.temperature}
|
||||
.min=${this._min}
|
||||
.max=${this._max}
|
||||
.step=${this._step}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"temperature"
|
||||
)}
|
||||
style=${styleMap({
|
||||
"--control-number-buttons-focus-color": stateColor,
|
||||
})}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-number-buttons>
|
||||
</ha-control-number-buttons>
|
||||
`;
|
||||
}
|
||||
|
||||
if (
|
||||
domain === "climate" &&
|
||||
supportsFeature(
|
||||
this.stateObj,
|
||||
ClimateEntityFeature.TARGET_TEMPERATURE_RANGE
|
||||
)
|
||||
) {
|
||||
return html`
|
||||
<ha-control-button-group>
|
||||
<ha-control-number-buttons
|
||||
.formatOptions=${options}
|
||||
.target=${"low"}
|
||||
.value=${(this.stateObj as ClimateEntity).attributes.target_temp_low}
|
||||
.min=${this._min}
|
||||
.max=${Math.min(this._max, this._targetTemperature.high ?? this._max)}
|
||||
.step=${this._step}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"temperature"
|
||||
)}
|
||||
style=${styleMap({
|
||||
"--control-number-buttons-focus-color": stateColor,
|
||||
})}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-number-buttons>
|
||||
<ha-control-number-buttons
|
||||
.formatOptions=${options}
|
||||
.target=${"high"}
|
||||
.value=${(this.stateObj as ClimateEntity).attributes.target_temp_high}
|
||||
.min=${Math.max(this._min, this._targetTemperature.low ?? this._min)}
|
||||
.max=${this._max}
|
||||
.step=${this._step}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass.formatEntityAttributeName(
|
||||
this.stateObj,
|
||||
"temperature"
|
||||
)}
|
||||
style=${styleMap({
|
||||
"--control-number-buttons-focus-color": stateColor,
|
||||
})}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-number-buttons>
|
||||
</ha-control-number-buttons>
|
||||
`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return css`
|
||||
ha-control-button-group {
|
||||
margin: 0 12px 12px 12px;
|
||||
--control-button-group-spacing: 12px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-target-temperature-tile-feature": HuiTargetTemperatureTileFeature;
|
||||
}
|
||||
}
|
@ -40,6 +40,10 @@ export interface ClimateHvacModesTileFeatureConfig {
|
||||
hvac_modes?: HvacMode[];
|
||||
}
|
||||
|
||||
export interface TargetTemperatureTileFeatureConfig {
|
||||
type: "target-temperature";
|
||||
}
|
||||
|
||||
export interface WaterHeaterOperationModesTileFeatureConfig {
|
||||
type: "water-heater-operation-modes";
|
||||
operation_modes?: OperationMode[];
|
||||
@ -81,6 +85,7 @@ export type LovelaceTileFeatureConfig =
|
||||
| LightBrightnessTileFeatureConfig
|
||||
| LightColorTempTileFeatureConfig
|
||||
| VacuumCommandsTileFeatureConfig
|
||||
| TargetTemperatureTileFeatureConfig
|
||||
| WaterHeaterOperationModesTileFeatureConfig;
|
||||
|
||||
export type LovelaceTileFeatureContext = {
|
||||
|
@ -5031,6 +5031,9 @@
|
||||
"label": "Climate HVAC modes",
|
||||
"hvac_modes": "HVAC modes"
|
||||
},
|
||||
"target-temperature": {
|
||||
"label": "Target temperature"
|
||||
},
|
||||
"water-heater-operation-modes": {
|
||||
"label": "Water heater operation modes",
|
||||
"operation_modes": "Operation modes"
|
||||
|
Loading…
x
Reference in New Issue
Block a user