mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-25 18:26:35 +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
|
// Variant that only applies the clamping to a border if the border is defined
|
||||||
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
export const conditionalClamp = (value: number, min?: number, max?: number) => {
|
||||||
let result: number;
|
let result: number;
|
||||||
result = min ? Math.max(value, min) : value;
|
result = min != null ? Math.max(value, min) : value;
|
||||||
result = max ? Math.min(result, max) : result;
|
result = max != null ? Math.min(result, max) : result;
|
||||||
return 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 { customElement, property, state } from "lit/decorators";
|
||||||
import { classMap } from "lit/directives/class-map";
|
import { classMap } from "lit/directives/class-map";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { UNIT_F } from "../../../../common/const";
|
||||||
import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
|
import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display";
|
||||||
import { stateActive } from "../../../../common/entity/state_active";
|
import { stateActive } from "../../../../common/entity/state_active";
|
||||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||||
@ -67,7 +68,7 @@ export class HaMoreInfoClimateTemperature extends LitElement {
|
|||||||
private get _step() {
|
private get _step() {
|
||||||
return (
|
return (
|
||||||
this.stateObj.attributes.target_temp_step ||
|
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";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { styleMap } from "lit/directives/style-map";
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import { UNIT_F } from "../../../../common/const";
|
||||||
import { stateActive } from "../../../../common/entity/state_active";
|
import { stateActive } from "../../../../common/entity/state_active";
|
||||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||||
@ -44,7 +45,7 @@ export class HaMoreInfoWaterHeaterTemperature extends LitElement {
|
|||||||
private get _step() {
|
private get _step() {
|
||||||
return (
|
return (
|
||||||
this.stateObj.attributes.target_temp_step ||
|
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-alarm-modes-tile-feature";
|
||||||
import "../tile-features/hui-climate-hvac-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-open-close-tile-feature";
|
||||||
import "../tile-features/hui-cover-position-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-position-tile-feature";
|
||||||
import "../tile-features/hui-cover-tilt-tile-feature";
|
import "../tile-features/hui-cover-tilt-tile-feature";
|
||||||
import "../tile-features/hui-fan-speed-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-brightness-tile-feature";
|
||||||
import "../tile-features/hui-light-color-temp-tile-feature";
|
import "../tile-features/hui-light-color-temp-tile-feature";
|
||||||
import "../tile-features/hui-vacuum-commands-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 "../tile-features/hui-water-heater-operation-modes-tile-feature";
|
||||||
import { LovelaceTileFeatureConfig } from "../tile-features/types";
|
import { LovelaceTileFeatureConfig } from "../tile-features/types";
|
||||||
import {
|
import {
|
||||||
@ -17,17 +18,18 @@ import {
|
|||||||
} from "./create-element-base";
|
} from "./create-element-base";
|
||||||
|
|
||||||
const TYPES: Set<LovelaceTileFeatureConfig["type"]> = new Set([
|
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",
|
"alarm-modes",
|
||||||
"climate-hvac-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",
|
"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 { supportsVacuumCommandTileFeature } from "../../tile-features/hui-vacuum-commands-tile-feature";
|
||||||
import { supportsWaterHeaterOperationModesTileFeature } from "../../tile-features/hui-water-heater-operation-modes-tile-feature";
|
import { supportsWaterHeaterOperationModesTileFeature } from "../../tile-features/hui-water-heater-operation-modes-tile-feature";
|
||||||
import { LovelaceTileFeatureConfig } from "../../tile-features/types";
|
import { LovelaceTileFeatureConfig } from "../../tile-features/types";
|
||||||
|
import { supportsTargetTemperatureTileFeature } from "../../tile-features/hui-target-temperature-tile-feature";
|
||||||
|
|
||||||
type FeatureType = LovelaceTileFeatureConfig["type"];
|
type FeatureType = LovelaceTileFeatureConfig["type"];
|
||||||
type SupportsFeature = (stateObj: HassEntity) => boolean;
|
type SupportsFeature = (stateObj: HassEntity) => boolean;
|
||||||
@ -44,6 +45,7 @@ type SupportsFeature = (stateObj: HassEntity) => boolean;
|
|||||||
const FEATURE_TYPES: FeatureType[] = [
|
const FEATURE_TYPES: FeatureType[] = [
|
||||||
"alarm-modes",
|
"alarm-modes",
|
||||||
"climate-hvac-modes",
|
"climate-hvac-modes",
|
||||||
|
"target-temperature",
|
||||||
"cover-open-close",
|
"cover-open-close",
|
||||||
"cover-position",
|
"cover-position",
|
||||||
"cover-tilt-position",
|
"cover-tilt-position",
|
||||||
@ -76,6 +78,7 @@ const SUPPORTS_FEATURE_TYPES: Record<FeatureType, SupportsFeature | undefined> =
|
|||||||
"lawn-mower-commands": supportsLawnMowerCommandTileFeature,
|
"lawn-mower-commands": supportsLawnMowerCommandTileFeature,
|
||||||
"light-brightness": supportsLightBrightnessTileFeature,
|
"light-brightness": supportsLightBrightnessTileFeature,
|
||||||
"light-color-temp": supportsLightColorTempTileFeature,
|
"light-color-temp": supportsLightColorTempTileFeature,
|
||||||
|
"target-temperature": supportsTargetTemperatureTileFeature,
|
||||||
"vacuum-commands": supportsVacuumCommandTileFeature,
|
"vacuum-commands": supportsVacuumCommandTileFeature,
|
||||||
"water-heater-operation-modes":
|
"water-heater-operation-modes":
|
||||||
supportsWaterHeaterOperationModesTileFeature,
|
supportsWaterHeaterOperationModesTileFeature,
|
||||||
@ -151,8 +154,10 @@ export class HuiTileCardFeaturesEditor extends LitElement {
|
|||||||
const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType];
|
const customFeatureEntry = CUSTOM_FEATURE_ENTRIES[customType];
|
||||||
return customFeatureEntry?.name || type;
|
return customFeatureEntry?.name || type;
|
||||||
}
|
}
|
||||||
return this.hass!.localize(
|
return (
|
||||||
`ui.panel.lovelace.editor.card.tile.features.types.${type}.label`
|
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[];
|
hvac_modes?: HvacMode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TargetTemperatureTileFeatureConfig {
|
||||||
|
type: "target-temperature";
|
||||||
|
}
|
||||||
|
|
||||||
export interface WaterHeaterOperationModesTileFeatureConfig {
|
export interface WaterHeaterOperationModesTileFeatureConfig {
|
||||||
type: "water-heater-operation-modes";
|
type: "water-heater-operation-modes";
|
||||||
operation_modes?: OperationMode[];
|
operation_modes?: OperationMode[];
|
||||||
@ -81,6 +85,7 @@ export type LovelaceTileFeatureConfig =
|
|||||||
| LightBrightnessTileFeatureConfig
|
| LightBrightnessTileFeatureConfig
|
||||||
| LightColorTempTileFeatureConfig
|
| LightColorTempTileFeatureConfig
|
||||||
| VacuumCommandsTileFeatureConfig
|
| VacuumCommandsTileFeatureConfig
|
||||||
|
| TargetTemperatureTileFeatureConfig
|
||||||
| WaterHeaterOperationModesTileFeatureConfig;
|
| WaterHeaterOperationModesTileFeatureConfig;
|
||||||
|
|
||||||
export type LovelaceTileFeatureContext = {
|
export type LovelaceTileFeatureContext = {
|
||||||
|
@ -5031,6 +5031,9 @@
|
|||||||
"label": "Climate HVAC modes",
|
"label": "Climate HVAC modes",
|
||||||
"hvac_modes": "HVAC modes"
|
"hvac_modes": "HVAC modes"
|
||||||
},
|
},
|
||||||
|
"target-temperature": {
|
||||||
|
"label": "Target temperature"
|
||||||
|
},
|
||||||
"water-heater-operation-modes": {
|
"water-heater-operation-modes": {
|
||||||
"label": "Water heater operation modes",
|
"label": "Water heater operation modes",
|
||||||
"operation_modes": "Operation modes"
|
"operation_modes": "Operation modes"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user