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:
Paul Bottein 2023-03-27 10:11:28 +02:00 committed by GitHub
parent 8ff56bd8f5
commit 8ea350a488
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 919 additions and 124 deletions

View File

@ -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;

View File

@ -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%;

View File

@ -1,4 +1,5 @@
export const STATE_ATTRIBUTES = [
"entity_id",
"assumed_state",
"attribution",
"custom_ui_more_info",

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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",

View File

@ -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);
}
`;
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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"