mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-24 09:46:36 +00:00
Add cover new more info (#15694)
* Add cover new more info * Improve tilt cover control * Improve controls * Better handle group * Refactor toggle component * Use deep purple for cover and adjust deep purple color * Update purple color * Feedbacks * Change color * Improve tilt backgroud constrast
This commit is contained in:
parent
8ff56bd8f5
commit
8ea350a488
@ -1,6 +1,7 @@
|
||||
/** Return an color representing a state. */
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { UNAVAILABLE } from "../../data/entity";
|
||||
import { computeGroupDomain, GroupEntity } from "../../data/group";
|
||||
import { computeCssVariable } from "../../resources/css-variables";
|
||||
import { slugify } from "../string/slugify";
|
||||
import { batteryStateColorProperty } from "./color/battery_color";
|
||||
@ -52,11 +53,11 @@ export const stateColorCss = (stateObj: HassEntity, state?: string) => {
|
||||
};
|
||||
|
||||
export const domainStateColorProperties = (
|
||||
domain: string,
|
||||
stateObj: HassEntity,
|
||||
state?: string
|
||||
): string[] => {
|
||||
const compareState = state !== undefined ? state : stateObj.state;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
const active = stateActive(stateObj, state);
|
||||
|
||||
const properties: string[] = [];
|
||||
@ -95,8 +96,16 @@ export const stateColorProperties = (
|
||||
}
|
||||
}
|
||||
|
||||
// Special rules for group coloring
|
||||
if (domain === "group") {
|
||||
const groupDomain = computeGroupDomain(stateObj as GroupEntity);
|
||||
if (groupDomain && STATE_COLORED_DOMAIN.has(groupDomain)) {
|
||||
return domainStateColorProperties(groupDomain, stateObj, state);
|
||||
}
|
||||
}
|
||||
|
||||
if (STATE_COLORED_DOMAIN.has(domain)) {
|
||||
return domainStateColorProperties(stateObj, state);
|
||||
return domainStateColorProperties(domain, stateObj, state);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
|
@ -244,6 +244,7 @@ export class HaControlSlider extends LitElement {
|
||||
})}
|
||||
>
|
||||
<div class="slider-track-background"></div>
|
||||
<slot name="background"></slot>
|
||||
${this.mode === "cursor"
|
||||
? this.value != null
|
||||
? html`
|
||||
@ -310,6 +311,13 @@ export class HaControlSlider extends LitElement {
|
||||
background: var(--control-slider-background);
|
||||
opacity: var(--control-slider-background-opacity);
|
||||
}
|
||||
::slotted([slot="background"]) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.slider .slider-track-bar {
|
||||
--border-radius: var(--control-slider-border-radius);
|
||||
--handle-size: 4px;
|
||||
@ -424,6 +432,7 @@ export class HaControlSlider extends LitElement {
|
||||
bottom: 0;
|
||||
left: calc(var(--value, 0) * (100% - var(--cursor-size)));
|
||||
width: var(--cursor-size);
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.slider .slider-track-cursor:after {
|
||||
height: 50%;
|
||||
|
@ -1,4 +1,5 @@
|
||||
export const STATE_ATTRIBUTES = [
|
||||
"entity_id",
|
||||
"assumed_state",
|
||||
"attribution",
|
||||
"custom_ui_more_info",
|
||||
|
@ -0,0 +1,311 @@
|
||||
import { mdiArrowBottomLeft, mdiArrowTopRight, mdiStop } from "@mdi/js";
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import {
|
||||
computeCloseIcon,
|
||||
computeOpenIcon,
|
||||
} from "../../../../common/entity/cover_icon";
|
||||
import { supportsFeature } from "../../../../common/entity/supports-feature";
|
||||
import "../../../../components/ha-control-button";
|
||||
import "../../../../components/ha-control-button-group";
|
||||
import "../../../../components/ha-control-slider";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import {
|
||||
canClose,
|
||||
canCloseTilt,
|
||||
canOpen,
|
||||
canOpenTilt,
|
||||
canStop,
|
||||
canStopTilt,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
} from "../../../../data/cover";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
type CoverButton =
|
||||
| "open"
|
||||
| "close"
|
||||
| "stop"
|
||||
| "open-tilt"
|
||||
| "close-tilt"
|
||||
| "none";
|
||||
|
||||
type CoverLayout = {
|
||||
type: "line" | "cross";
|
||||
buttons: CoverButton[];
|
||||
};
|
||||
|
||||
export const getCoverLayout = memoizeOne(
|
||||
(stateObj: CoverEntity): CoverLayout => {
|
||||
const supportsOpen = supportsFeature(stateObj, CoverEntityFeature.OPEN);
|
||||
const supportsClose = supportsFeature(stateObj, CoverEntityFeature.CLOSE);
|
||||
const supportsStop = supportsFeature(stateObj, CoverEntityFeature.STOP);
|
||||
const supportsOpenTilt = supportsFeature(
|
||||
stateObj,
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
);
|
||||
const supportsCloseTilt = supportsFeature(
|
||||
stateObj,
|
||||
CoverEntityFeature.CLOSE_TILT
|
||||
);
|
||||
const supportsStopTilt = supportsFeature(
|
||||
stateObj,
|
||||
CoverEntityFeature.STOP_TILT
|
||||
);
|
||||
|
||||
if (
|
||||
(supportsOpen || supportsClose) &&
|
||||
(supportsOpenTilt || supportsCloseTilt)
|
||||
) {
|
||||
return {
|
||||
type: "cross",
|
||||
buttons: [
|
||||
supportsOpen ? "open" : "none",
|
||||
supportsCloseTilt ? "close-tilt" : "none",
|
||||
supportsStop || supportsStopTilt ? "stop" : "none",
|
||||
supportsOpenTilt ? "open-tilt" : "none",
|
||||
supportsClose ? "close" : "none",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
if (supportsOpen || supportsClose) {
|
||||
const buttons: CoverButton[] = [];
|
||||
if (supportsOpen) buttons.push("open");
|
||||
if (supportsStop) buttons.push("stop");
|
||||
if (supportsClose) buttons.push("close");
|
||||
return {
|
||||
type: "line",
|
||||
buttons,
|
||||
};
|
||||
}
|
||||
|
||||
if (supportsOpenTilt || supportsCloseTilt) {
|
||||
const buttons: CoverButton[] = [];
|
||||
if (supportsOpenTilt) buttons.push("open-tilt");
|
||||
if (supportsStopTilt) buttons.push("stop");
|
||||
if (supportsCloseTilt) buttons.push("close-tilt");
|
||||
return {
|
||||
type: "line",
|
||||
buttons,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: "line",
|
||||
buttons: [],
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@customElement("ha-more-info-cover-buttons")
|
||||
export class HaMoreInfoCoverButtons extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
private _onOpenTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this.hass!.callService("cover", "open_cover", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this.hass!.callService("cover", "close_cover", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onOpenTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this.hass!.callService("cover", "open_cover_tilt", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onCloseTiltTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
this.hass!.callService("cover", "close_cover_tilt", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
|
||||
private _onStopTap(ev): void {
|
||||
ev.stopPropagation();
|
||||
if (supportsFeature(this.stateObj, CoverEntityFeature.STOP)) {
|
||||
this.hass!.callService("cover", "stop_cover", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
if (supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT)) {
|
||||
this.hass!.callService("cover", "stop_cover_tilt", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
protected renderButton(button: CoverButton | undefined) {
|
||||
if (button === "open") {
|
||||
return html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.cover.open_cover"
|
||||
)}
|
||||
@click=${this._onOpenTap}
|
||||
.disabled=${!canOpen(this.stateObj)}
|
||||
data-button="open"
|
||||
>
|
||||
<ha-svg-icon .path=${computeOpenIcon(this.stateObj)}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
}
|
||||
if (button === "close") {
|
||||
return html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.cover.close_cover"
|
||||
)}
|
||||
@click=${this._onCloseTap}
|
||||
.disabled=${!canClose(this.stateObj)}
|
||||
data-button="close"
|
||||
>
|
||||
<ha-svg-icon .path=${computeCloseIcon(this.stateObj)}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
}
|
||||
if (button === "stop") {
|
||||
return html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.cover.stop_cover"
|
||||
)}
|
||||
@click=${this._onStopTap}
|
||||
.disabled=${!canStop(this.stateObj) && !canStopTilt(this.stateObj)}
|
||||
data-button="stop"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiStop}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
}
|
||||
if (button === "open-tilt") {
|
||||
return html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.cover.open_tilt_cover"
|
||||
)}
|
||||
@click=${this._onOpenTiltTap}
|
||||
.disabled=${!canOpenTilt(this.stateObj)}
|
||||
data-button="open-tilt"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiArrowTopRight}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
}
|
||||
if (button === "close-tilt") {
|
||||
return html`
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.cover.close_tilt_cover"
|
||||
)}
|
||||
@click=${this._onCloseTiltTap}
|
||||
.disabled=${!canCloseTilt(this.stateObj)}
|
||||
data-button="close-tilt"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiArrowBottomLeft}></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const layout = getCoverLayout(this.stateObj);
|
||||
|
||||
return html`
|
||||
${layout.type === "line"
|
||||
? html`
|
||||
<ha-control-button-group vertical>
|
||||
${repeat(
|
||||
layout.buttons,
|
||||
(action) => action,
|
||||
(action) => this.renderButton(action)
|
||||
)}
|
||||
</ha-control-button-group>
|
||||
`
|
||||
: nothing}
|
||||
${layout.type === "cross"
|
||||
? html`
|
||||
<div class="cross-container">
|
||||
${repeat(
|
||||
layout.buttons,
|
||||
(action) => action,
|
||||
(action) => this.renderButton(action)
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-control-button-group {
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
--control-button-group-spacing: 6px;
|
||||
--control-button-group-thickness: 100px;
|
||||
}
|
||||
.cross-container {
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(3, min(100px, 25vw, 15vh));
|
||||
grid-template-rows: repeat(3, min(100px, 25vw, 15vh));
|
||||
grid-template-areas: ". open ." "close-tilt stop open-tilt" ". close .";
|
||||
}
|
||||
.cross-container > * {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.cross-container > [data-button="open"] {
|
||||
grid-area: open;
|
||||
}
|
||||
.cross-container > [data-button="close"] {
|
||||
grid-area: close;
|
||||
}
|
||||
.cross-container > [data-button="open-tilt"] {
|
||||
grid-area: open-tilt;
|
||||
}
|
||||
.cross-container > [data-button="close-tilt"] {
|
||||
grid-area: close-tilt;
|
||||
}
|
||||
.cross-container > [data-button="stop"] {
|
||||
grid-area: stop;
|
||||
}
|
||||
ha-control-button {
|
||||
--control-button-border-radius: 18px;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-cover-buttons": HaMoreInfoCoverButtons;
|
||||
}
|
||||
}
|
@ -0,0 +1,87 @@
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import "../../../../components/ha-control-slider";
|
||||
import { CoverEntity } from "../../../../data/cover";
|
||||
import { UNAVAILABLE } from "../../../../data/entity";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
@customElement("ha-more-info-cover-position")
|
||||
export class HaMoreInfoCoverPosition extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
@state() value?: number;
|
||||
|
||||
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
|
||||
if (changedProp.has("stateObj")) {
|
||||
const currentPosition = this.stateObj?.attributes.current_position;
|
||||
this.value =
|
||||
currentPosition != null ? Math.round(currentPosition) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const value = (ev.detail as any).value;
|
||||
if (isNaN(value)) return;
|
||||
|
||||
this.hass.callService("cover", "set_cover_position", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
position: value,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const color = stateColorCss(this.stateObj);
|
||||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
vertical
|
||||
.value=${this.value}
|
||||
min="0"
|
||||
max="100"
|
||||
show-handle
|
||||
mode="end"
|
||||
@value-changed=${this._valueChanged}
|
||||
.ariaLabel=${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.entities,
|
||||
"position"
|
||||
)}
|
||||
style=${styleMap({
|
||||
"--control-slider-color": color,
|
||||
"--control-slider-background": color,
|
||||
})}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-slider>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-control-slider {
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
/* Force inactive state to be colored for the slider */
|
||||
--state-cover-inactive-color: var(--state-cover-active-color);
|
||||
--control-slider-thickness: 100px;
|
||||
--control-slider-border-radius: 24px;
|
||||
--control-slider-color: var(--primary-color);
|
||||
--control-slider-background: var(--disabled-color);
|
||||
--control-slider-background-opacity: 0.2;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-cover-position": HaMoreInfoCoverPosition;
|
||||
}
|
||||
}
|
@ -0,0 +1,128 @@
|
||||
import {
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
TemplateResult,
|
||||
unsafeCSS,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import "../../../../components/ha-control-slider";
|
||||
import { CoverEntity } from "../../../../data/cover";
|
||||
import { UNAVAILABLE } from "../../../../data/entity";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
function generateTiltSliderTrackBackgroundGradient() {
|
||||
const count = 24;
|
||||
const minStrokeWidth = 0.2;
|
||||
const gradient: [number, string][] = [];
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const stopOffset1 = i / count;
|
||||
const stopOffset2 =
|
||||
stopOffset1 +
|
||||
(i / count ** 2) * (1 - minStrokeWidth) +
|
||||
minStrokeWidth / count;
|
||||
|
||||
if (i !== 0) {
|
||||
gradient.push([stopOffset1, "transparent"]);
|
||||
}
|
||||
gradient.push([stopOffset1, "var(--control-slider-color)"]);
|
||||
gradient.push([stopOffset2, "var(--control-slider-color)"]);
|
||||
gradient.push([stopOffset2, "transparent"]);
|
||||
}
|
||||
|
||||
return unsafeCSS(
|
||||
gradient
|
||||
.map(([stop, color]) => `${color} ${(stop as number) * 100}%`)
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
const GRADIENT = generateTiltSliderTrackBackgroundGradient();
|
||||
|
||||
@customElement("ha-more-info-cover-tilt-position")
|
||||
export class HaMoreInfoCoverTiltPosition extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
@state() value?: number;
|
||||
|
||||
protected updated(changedProp: Map<string | number | symbol, unknown>): void {
|
||||
if (changedProp.has("stateObj")) {
|
||||
this.value =
|
||||
this.stateObj.attributes.current_tilt_position != null
|
||||
? Math.round(this.stateObj.attributes.current_tilt_position)
|
||||
: undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent) {
|
||||
const value = (ev.detail as any).value;
|
||||
if (isNaN(value)) return;
|
||||
|
||||
this.hass.callService("cover", "set_cover_tilt_position", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
tilt_position: value,
|
||||
});
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const color = stateColorCss(this.stateObj);
|
||||
const isUnavailable = this.stateObj.state === UNAVAILABLE;
|
||||
|
||||
return html`
|
||||
<ha-control-slider
|
||||
vertical
|
||||
.value=${this.value}
|
||||
min="0"
|
||||
max="100"
|
||||
mode="cursor"
|
||||
@value-changed=${this._valueChanged}
|
||||
.ariaLabel=${computeAttributeNameDisplay(
|
||||
this.hass.localize,
|
||||
this.stateObj,
|
||||
this.hass.entities,
|
||||
"tilt_position"
|
||||
)}
|
||||
style=${styleMap({
|
||||
"--control-slider-color": color,
|
||||
})}
|
||||
.disabled=${isUnavailable}
|
||||
>
|
||||
<div slot="background" class="gradient"></div>
|
||||
</ha-control-slider>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-control-slider {
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
/* Force inactive state to be colored for the slider */
|
||||
--state-cover-inactive-color: var(--state-cover-active-color);
|
||||
--control-slider-thickness: 100px;
|
||||
--control-slider-border-radius: 24px;
|
||||
--control-slider-color: var(--primary-color);
|
||||
--control-slider-background-opacity: 0.2;
|
||||
--control-slider-background: var(--control-slider-color);
|
||||
}
|
||||
.gradient {
|
||||
background: -webkit-linear-gradient(top, ${GRADIENT});
|
||||
opacity: 0.6;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-cover-tilt-position": HaMoreInfoCoverTiltPosition;
|
||||
}
|
||||
}
|
@ -0,0 +1,169 @@
|
||||
import { HassEntity } from "home-assistant-js-websocket";
|
||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { domainIcon } from "../../../../common/entity/domain_icon";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import "../../../../components/ha-control-button";
|
||||
import "../../../../components/ha-control-switch";
|
||||
import { UNAVAILABLE, UNKNOWN } from "../../../../data/entity";
|
||||
import { forwardHaptic } from "../../../../data/haptics";
|
||||
import { HomeAssistant } from "../../../../types";
|
||||
|
||||
@customElement("ha-more-info-cover-toggle")
|
||||
export class HaMoreInfoCoverToggle extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: HassEntity;
|
||||
|
||||
private _valueChanged(ev) {
|
||||
const checked = ev.target.checked as boolean;
|
||||
|
||||
if (checked) {
|
||||
this._turnOn();
|
||||
} else {
|
||||
this._turnOff();
|
||||
}
|
||||
}
|
||||
|
||||
private _turnOn() {
|
||||
this._callService(true);
|
||||
}
|
||||
|
||||
private _turnOff() {
|
||||
this._callService(false);
|
||||
}
|
||||
|
||||
private async _callService(turnOn): Promise<void> {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return;
|
||||
}
|
||||
forwardHaptic("light");
|
||||
|
||||
await this.hass.callService(
|
||||
"cover",
|
||||
turnOn ? "open_cover" : "close_cover",
|
||||
{
|
||||
entity_id: this.stateObj.entity_id,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const onColor = stateColorCss(this.stateObj, "open");
|
||||
const offColor = stateColorCss(this.stateObj, "closed");
|
||||
|
||||
const isOn =
|
||||
this.stateObj.state === "open" ||
|
||||
this.stateObj.state === "closing" ||
|
||||
this.stateObj.state === "opening";
|
||||
const isOff = this.stateObj.state === "closed";
|
||||
|
||||
if (
|
||||
this.stateObj.attributes.assumed_state ||
|
||||
this.stateObj.state === UNKNOWN
|
||||
) {
|
||||
return html`
|
||||
<div class="buttons">
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.cover.open_cover"
|
||||
)}
|
||||
@click=${this._turnOn}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
class=${classMap({
|
||||
active: isOn,
|
||||
})}
|
||||
style=${styleMap({
|
||||
"--color": onColor,
|
||||
})}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${domainIcon("cover", this.stateObj, "open")}
|
||||
></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
<ha-control-button
|
||||
.label=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.cover.close_cover"
|
||||
)}
|
||||
@click=${this._turnOff}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
class=${classMap({
|
||||
active: isOff,
|
||||
})}
|
||||
style=${styleMap({
|
||||
"--color": offColor,
|
||||
})}
|
||||
>
|
||||
<ha-svg-icon
|
||||
.path=${domainIcon("cover", this.stateObj, "closed")}
|
||||
></ha-svg-icon>
|
||||
</ha-control-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-control-switch
|
||||
.pathOn=${domainIcon("cover", this.stateObj, "open")}
|
||||
.pathOff=${domainIcon("cover", this.stateObj, "closed")}
|
||||
vertical
|
||||
reversed
|
||||
.checked=${isOn}
|
||||
@change=${this._valueChanged}
|
||||
.ariaLabel=${this.hass.localize("ui.dialogs.more_info_control.toggle")}
|
||||
style=${styleMap({
|
||||
"--control-switch-on-color": onColor,
|
||||
"--control-switch-off-color": offColor,
|
||||
})}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-switch>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return css`
|
||||
ha-control-switch {
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
--control-switch-thickness: 100px;
|
||||
--control-switch-border-radius: 24px;
|
||||
--control-switch-padding: 6px;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100px;
|
||||
height: 45vh;
|
||||
max-height: 320px;
|
||||
min-height: 200px;
|
||||
padding: 6px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
ha-control-button {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
--control-button-border-radius: 18px;
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
ha-control-button.active {
|
||||
--control-button-icon-color: white;
|
||||
--control-button-background-color: var(--color);
|
||||
--control-button-background-opacity: 1;
|
||||
}
|
||||
ha-control-button:not(:last-child) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-cover-toggle": HaMoreInfoCoverToggle;
|
||||
}
|
||||
}
|
@ -14,6 +14,10 @@ export const moreInfoControlStyle = css`
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controls:not(:last-child) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.controls > *:not(:last-child) {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ export const EDITABLE_DOMAINS_WITH_ID = ["scene", "automation"];
|
||||
export const EDITABLE_DOMAINS_WITH_UNIQUE_ID = ["script"];
|
||||
/** Domains with with new more info design. */
|
||||
export const DOMAINS_WITH_NEW_MORE_INFO = [
|
||||
"cover",
|
||||
"fan",
|
||||
"input_boolean",
|
||||
"light",
|
||||
|
@ -1,89 +1,195 @@
|
||||
import { css, CSSResult, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { attributeClassNames } from "../../../common/entity/attribute_class_names";
|
||||
import { mdiMenu, mdiSwapVertical } from "@mdi/js";
|
||||
import {
|
||||
FeatureClassNames,
|
||||
featureClassNames,
|
||||
} from "../../../common/entity/feature_class_names";
|
||||
css,
|
||||
CSSResultGroup,
|
||||
html,
|
||||
LitElement,
|
||||
nothing,
|
||||
PropertyValues,
|
||||
} from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import { blankBeforePercent } from "../../../common/translations/blank_before_percent";
|
||||
import "../../../components/ha-attributes";
|
||||
import "../../../components/ha-cover-tilt-controls";
|
||||
import "../../../components/ha-labeled-slider";
|
||||
import {
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
isTiltOnly,
|
||||
} from "../../../data/cover";
|
||||
import { HomeAssistant } from "../../../types";
|
||||
|
||||
export const FEATURE_CLASS_NAMES: FeatureClassNames<CoverEntityFeature> = {
|
||||
[CoverEntityFeature.SET_POSITION]: "has-set_position",
|
||||
[CoverEntityFeature.OPEN_TILT]: "has-open_tilt",
|
||||
[CoverEntityFeature.CLOSE_TILT]: "has-close_tilt",
|
||||
[CoverEntityFeature.STOP_TILT]: "has-stop_tilt",
|
||||
[CoverEntityFeature.SET_TILT_POSITION]: "has-set_tilt_position",
|
||||
};
|
||||
import { CoverEntity, CoverEntityFeature } from "../../../data/cover";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/cover/ha-more-info-cover-buttons";
|
||||
import "../components/cover/ha-more-info-cover-position";
|
||||
import "../components/cover/ha-more-info-cover-tilt-position";
|
||||
import "../components/cover/ha-more-info-cover-toggle";
|
||||
import { moreInfoControlStyle } from "../components/ha-more-info-control-style";
|
||||
import "../components/ha-more-info-state-header";
|
||||
|
||||
@customElement("more-info-cover")
|
||||
class MoreInfoCover extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
@property({ attribute: false }) public stateObj?: CoverEntity;
|
||||
|
||||
@state() private _displayedPosition?: number;
|
||||
|
||||
@state() private _mode?: "position" | "button";
|
||||
|
||||
private _toggleMode() {
|
||||
this._mode = this._mode === "position" ? "button" : "position";
|
||||
}
|
||||
|
||||
private _positionChanged(ev) {
|
||||
const value = (ev.detail as any).value;
|
||||
if (isNaN(value)) return;
|
||||
this._displayedPosition = value;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("stateObj") && this.stateObj) {
|
||||
if (supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION)) {
|
||||
const currentPosition = this.stateObj?.attributes.current_position;
|
||||
this._displayedPosition =
|
||||
currentPosition != null ? Math.round(currentPosition) : undefined;
|
||||
}
|
||||
if (!this._mode) {
|
||||
this._mode =
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) ||
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION)
|
||||
? "position"
|
||||
: "button";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private get _stateOverride() {
|
||||
if (this._displayedPosition == null) return undefined;
|
||||
|
||||
const tempState = {
|
||||
...this.stateObj,
|
||||
state: this._displayedPosition ? "open" : "closed",
|
||||
attributes: {
|
||||
...this.stateObj!.attributes,
|
||||
current_position: this._displayedPosition,
|
||||
},
|
||||
} as CoverEntity;
|
||||
|
||||
const stateDisplay = computeStateDisplay(
|
||||
this.hass.localize,
|
||||
tempState!,
|
||||
this.hass.locale,
|
||||
this.hass.entities
|
||||
);
|
||||
|
||||
return this._displayedPosition && this._displayedPosition !== 100
|
||||
? `${stateDisplay} - ${Math.round(
|
||||
this._displayedPosition
|
||||
)}${blankBeforePercent(this.hass!.locale)}%`
|
||||
: stateDisplay;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
if (!this.hass || !this.stateObj) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const _isTiltOnly = isTiltOnly(this.stateObj);
|
||||
const supportsPosition = supportsFeature(
|
||||
this.stateObj,
|
||||
CoverEntityFeature.SET_POSITION
|
||||
);
|
||||
|
||||
const supportsTiltPosition = supportsFeature(
|
||||
this.stateObj,
|
||||
CoverEntityFeature.SET_TILT_POSITION
|
||||
);
|
||||
|
||||
const supportsOpenClose =
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.OPEN) ||
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) ||
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.STOP);
|
||||
|
||||
const supportsTilt =
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) ||
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT) ||
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.STOP_TILT);
|
||||
|
||||
const supportsOpenCloseWithoutStop =
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.OPEN) &&
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.CLOSE) &&
|
||||
!supportsFeature(this.stateObj, CoverEntityFeature.STOP) &&
|
||||
!supportsFeature(this.stateObj, CoverEntityFeature.OPEN_TILT) &&
|
||||
!supportsFeature(this.stateObj, CoverEntityFeature.CLOSE_TILT);
|
||||
|
||||
return html`
|
||||
<div class=${this._computeClassNames(this.stateObj)}>
|
||||
<div class="current_position">
|
||||
<ha-labeled-slider
|
||||
.caption=${this.hass.localize("ui.card.cover.position")}
|
||||
pin=""
|
||||
.value=${this.stateObj.attributes.current_position}
|
||||
.disabled=${!supportsFeature(
|
||||
this.stateObj,
|
||||
CoverEntityFeature.SET_POSITION
|
||||
)}
|
||||
@change=${this._coverPositionSliderChanged}
|
||||
></ha-labeled-slider>
|
||||
</div>
|
||||
|
||||
<div class="tilt">
|
||||
${supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION)
|
||||
? // Either render the labeled slider and put the tilt buttons into its slot
|
||||
// or (if tilt position is not supported and therefore no slider is shown)
|
||||
// render a title <div> (same style as for a labeled slider) and directly put
|
||||
// the tilt controls on the more-info.
|
||||
html` <ha-labeled-slider
|
||||
.caption=${this.hass.localize("ui.card.cover.tilt_position")}
|
||||
pin=""
|
||||
extra=""
|
||||
.value=${this.stateObj.attributes.current_tilt_position}
|
||||
@change=${this._coverTiltPositionSliderChanged}
|
||||
>
|
||||
${!_isTiltOnly
|
||||
? html`<ha-cover-tilt-controls
|
||||
.hass=${this.hass}
|
||||
slot="extra"
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-cover-tilt-controls> `
|
||||
: nothing}
|
||||
</ha-labeled-slider>`
|
||||
: !_isTiltOnly
|
||||
? html`
|
||||
<div class="title">
|
||||
${this.hass.localize("ui.card.cover.tilt_position")}
|
||||
</div>
|
||||
<ha-cover-tilt-controls
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
></ha-cover-tilt-controls>
|
||||
`
|
||||
: nothing}
|
||||
<ha-more-info-state-header
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.stateOverride=${this._stateOverride}
|
||||
></ha-more-info-state-header>
|
||||
<div class="controls">
|
||||
<div class="main-control">
|
||||
${
|
||||
this._mode === "position"
|
||||
? html`
|
||||
${supportsPosition
|
||||
? html`
|
||||
<ha-more-info-cover-position
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
@slider-moved=${this._positionChanged}
|
||||
></ha-more-info-cover-position>
|
||||
`
|
||||
: nothing}
|
||||
${supportsTiltPosition
|
||||
? html`
|
||||
<ha-more-info-cover-tilt-position
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-more-info-cover-tilt-position>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._mode === "button"
|
||||
? html`
|
||||
${supportsOpenCloseWithoutStop
|
||||
? html`
|
||||
<ha-more-info-cover-toggle
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-more-info-cover-toggle>
|
||||
`
|
||||
: supportsOpenClose || supportsTilt
|
||||
? html`
|
||||
<ha-more-info-cover-buttons
|
||||
.stateObj=${this.stateObj}
|
||||
.hass=${this.hass}
|
||||
></ha-more-info-cover-buttons>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
(supportsPosition || supportsTiltPosition) &&
|
||||
(supportsOpenClose || supportsTilt)
|
||||
? html`
|
||||
<div class="actions">
|
||||
<ha-icon-button
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.cover.switch_mode.${
|
||||
this._mode || "position"
|
||||
}`
|
||||
)}
|
||||
.path=${this._mode === "position"
|
||||
? mdiSwapVertical
|
||||
: mdiMenu}
|
||||
@click=${this._toggleMode}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<ha-attributes
|
||||
@ -94,55 +200,20 @@ class MoreInfoCover extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _computeClassNames(stateObj) {
|
||||
const classes = [
|
||||
attributeClassNames(stateObj, [
|
||||
"current_position",
|
||||
"current_tilt_position",
|
||||
]),
|
||||
featureClassNames(stateObj, FEATURE_CLASS_NAMES),
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
moreInfoControlStyle,
|
||||
css`
|
||||
.main-control {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.main-control > * {
|
||||
margin: 0 8px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
return classes.join(" ");
|
||||
}
|
||||
|
||||
private _coverPositionSliderChanged(ev) {
|
||||
this.hass.callService("cover", "set_cover_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
position: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
private _coverTiltPositionSliderChanged(ev) {
|
||||
this.hass.callService("cover", "set_cover_tilt_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
tilt_position: ev.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResult {
|
||||
return css`
|
||||
.current_position,
|
||||
.tilt {
|
||||
max-height: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.has-set_position .current_position,
|
||||
.has-current_position .current_position,
|
||||
.has-open_tilt .tilt,
|
||||
.has-close_tilt .tilt,
|
||||
.has-stop_tilt .tilt,
|
||||
.has-set_tilt_position .tilt,
|
||||
.has-current_tilt_position .tilt {
|
||||
max-height: 208px;
|
||||
}
|
||||
|
||||
/* from ha-labeled-slider for consistent look */
|
||||
.title {
|
||||
margin: 5px 0 8px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,6 +59,7 @@ class MoreInfoGroup extends LitElement {
|
||||
attributes: {
|
||||
...baseStateObj.attributes,
|
||||
friendly_name: this.stateObj.attributes.friendly_name,
|
||||
entity_id: this.stateObj.attributes.entity_id,
|
||||
},
|
||||
};
|
||||
const type = domainMoreInfoType(groupDomain);
|
||||
|
@ -96,8 +96,8 @@ documentContainer.innerHTML = `<custom-style>
|
||||
--disabled-color: #bdbdbd;
|
||||
--red-color: #f44336;
|
||||
--pink-color: #e91e63;
|
||||
--purple-color: #9c27b0;
|
||||
--deep-purple-color: #673ab7;
|
||||
--purple-color: #926bc7;
|
||||
--deep-purple-color: #6e41ab;
|
||||
--indigo-color: #3f51b5;
|
||||
--blue-color: #2196f3;
|
||||
--light-blue-color: #03a9f4;
|
||||
|
@ -891,7 +891,11 @@
|
||||
"close_cover": "Close cover",
|
||||
"open_tilt_cover": "Open cover tilt",
|
||||
"close_tilt_cover": "Close cover tilt",
|
||||
"stop_cover": "Stop cover"
|
||||
"stop_cover": "Stop cover",
|
||||
"switch_mode": {
|
||||
"button": "Switch to button mode",
|
||||
"position": "Switch to position mode"
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
"graph_unit": "People home"
|
||||
|
Loading…
x
Reference in New Issue
Block a user