mirror of
https://github.com/home-assistant/frontend.git
synced 2026-04-02 00:27:49 +01:00
Cover favorites (#29997)
* Make favorites UI reusable * Setup cover favorites * Fix * Make generic * Remove * Move * Add type for keys * Remove * Move * Types * Types * Changes for tilt position * Cleanup * Add missing options * Replace cover preset features with favorites features Editor points to more info instead of allowing cusomisation of feature * Fix drag * Remove learn more * Add domains with favorites shared data * Support recent additions: reset and copy favorites * Move favorites logic into new file, futureproof for new domains * Await all * Refactor * Use copy for primary action * Allow empty lists * Rename * Use better ally labels * Move favorites options above and use divider * Move * Use proper DOM types * Use proper type * Use proper types * Cleanup * Type from param * Center align label * Only show labels if both show
This commit is contained in:
@@ -11,6 +11,7 @@ import "./ha-svg-icon";
|
||||
export interface ControlSelectOption {
|
||||
value: string;
|
||||
label?: string;
|
||||
ariaLabel?: string;
|
||||
icon?: TemplateResult;
|
||||
path?: string;
|
||||
}
|
||||
@@ -161,8 +162,8 @@ export class HaControlSelect extends LitElement {
|
||||
tabindex=${isSelected ? "0" : "-1"}
|
||||
.value=${option.value}
|
||||
aria-checked=${isSelected ? "true" : "false"}
|
||||
aria-label=${ifDefined(option.label)}
|
||||
title=${ifDefined(option.label)}
|
||||
aria-label=${ifDefined(option.ariaLabel ?? option.label)}
|
||||
title=${ifDefined(option.ariaLabel ?? option.label)}
|
||||
@click=${this._handleOptionClick}
|
||||
@focus=${this._handleOptionFocus}
|
||||
@mousedown=${this._handleOptionMouseDown}
|
||||
|
||||
@@ -18,6 +18,47 @@ export const enum CoverEntityFeature {
|
||||
SET_TILT_POSITION = 128,
|
||||
}
|
||||
|
||||
export const DEFAULT_COVER_FAVORITE_POSITIONS = [0, 25, 75, 100];
|
||||
|
||||
export const coverSupportsPosition = (stateObj: CoverEntity) =>
|
||||
supportsFeature(stateObj, CoverEntityFeature.SET_POSITION);
|
||||
|
||||
export const coverSupportsTiltPosition = (stateObj: CoverEntity) =>
|
||||
supportsFeature(stateObj, CoverEntityFeature.SET_TILT_POSITION);
|
||||
|
||||
export const coverSupportsAnyPosition = (stateObj: CoverEntity) =>
|
||||
coverSupportsPosition(stateObj) || coverSupportsTiltPosition(stateObj);
|
||||
|
||||
export const normalizeCoverFavoritePositions = (
|
||||
positions?: number[]
|
||||
): number[] => {
|
||||
if (!positions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const unique = new Set<number>();
|
||||
const normalized: number[] = [];
|
||||
|
||||
for (const position of positions) {
|
||||
const value = Number(position);
|
||||
|
||||
if (isNaN(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const clamped = Math.max(0, Math.min(100, Math.round(value)));
|
||||
|
||||
if (unique.has(clamped)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
unique.add(clamped);
|
||||
normalized.push(clamped);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
};
|
||||
|
||||
export function isFullyOpen(stateObj: CoverEntity) {
|
||||
if (stateObj.attributes.current_position !== undefined) {
|
||||
return stateObj.attributes.current_position === 100;
|
||||
|
||||
@@ -92,6 +92,33 @@ export interface LightEntityOptions {
|
||||
favorite_colors?: LightColor[];
|
||||
}
|
||||
|
||||
export type FavoriteOption =
|
||||
| "favorite_colors"
|
||||
| "favorite_positions"
|
||||
| "favorite_tilt_positions";
|
||||
|
||||
export type FavoritesDomain = "light" | "cover";
|
||||
|
||||
export type FavoriteOptionValue = LightColor[] | number[];
|
||||
|
||||
export const DOMAINS_WITH_FAVORITES: FavoritesDomain[] = ["light", "cover"];
|
||||
|
||||
export const isFavoritesDomain = (domain: string): domain is FavoritesDomain =>
|
||||
DOMAINS_WITH_FAVORITES.includes(domain as FavoritesDomain);
|
||||
|
||||
export const shouldShowFavoriteOptions = (
|
||||
values?: FavoriteOptionValue | null
|
||||
): boolean => values == null || values.length > 0;
|
||||
|
||||
export const hasCustomFavoriteOptionValues = (
|
||||
values?: FavoriteOptionValue | null
|
||||
): boolean => values != null;
|
||||
|
||||
export interface CoverEntityOptions {
|
||||
favorite_positions?: number[];
|
||||
favorite_tilt_positions?: number[];
|
||||
}
|
||||
|
||||
export interface NumberEntityOptions {
|
||||
unit_of_measurement?: string | null;
|
||||
}
|
||||
@@ -134,6 +161,7 @@ export interface EntityRegistryOptions {
|
||||
lock?: LockEntityOptions;
|
||||
weather?: WeatherEntityOptions;
|
||||
light?: LightEntityOptions;
|
||||
cover?: CoverEntityOptions;
|
||||
vacuum?: VacuumEntityOptions;
|
||||
switch_as_x?: SwitchAsXEntityOptions;
|
||||
conversation?: Record<string, unknown>;
|
||||
@@ -158,6 +186,7 @@ export interface EntityRegistryEntryUpdateParams {
|
||||
| CalendarEntityOptions
|
||||
| WeatherEntityOptions
|
||||
| LightEntityOptions
|
||||
| CoverEntityOptions
|
||||
| VacuumEntityOptions;
|
||||
aliases?: (string | null)[];
|
||||
labels?: string[];
|
||||
|
||||
@@ -116,6 +116,7 @@ class DialogBox extends LitElement {
|
||||
.label=${this._params.inputLabel
|
||||
? this._params.inputLabel
|
||||
: ""}
|
||||
.suffix=${this._params.inputSuffix}
|
||||
.type=${this._params.inputType
|
||||
? this._params.inputType
|
||||
: "text"}
|
||||
|
||||
@@ -22,6 +22,7 @@ export interface ConfirmationDialogParams extends BaseDialogBoxParams {
|
||||
|
||||
export interface PromptDialogParams extends BaseDialogBoxParams {
|
||||
inputLabel?: string;
|
||||
inputSuffix?: string;
|
||||
dismissText?: string;
|
||||
inputType?: string;
|
||||
defaultValue?: string;
|
||||
|
||||
@@ -0,0 +1,475 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-control-button";
|
||||
import type { CoverEntity } from "../../../../data/cover";
|
||||
import {
|
||||
DEFAULT_COVER_FAVORITE_POSITIONS,
|
||||
coverSupportsPosition,
|
||||
coverSupportsTiltPosition,
|
||||
normalizeCoverFavoritePositions,
|
||||
} from "../../../../data/cover";
|
||||
import { UNAVAILABLE } from "../../../../data/entity/entity";
|
||||
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity/entity_attributes";
|
||||
import type {
|
||||
CoverEntityOptions,
|
||||
ExtEntityRegistryEntry,
|
||||
} from "../../../../data/entity/entity_registry";
|
||||
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
showConfirmationDialog,
|
||||
showPromptDialog,
|
||||
} from "../../../generic/show-dialog-box";
|
||||
import "../ha-more-info-favorites";
|
||||
import type { HaMoreInfoFavorites } from "../ha-more-info-favorites";
|
||||
|
||||
type FavoriteKind = "position" | "tilt";
|
||||
|
||||
type FavoriteLocalizeKey =
|
||||
| "set"
|
||||
| "edit"
|
||||
| "delete"
|
||||
| "delete_confirm_title"
|
||||
| "delete_confirm_text"
|
||||
| "delete_confirm_action"
|
||||
| "add"
|
||||
| "edit_title"
|
||||
| "add_title";
|
||||
|
||||
const favoriteKindFromEvent = (ev: Event): FavoriteKind =>
|
||||
(ev.currentTarget as HTMLElement).dataset.kind as FavoriteKind;
|
||||
|
||||
@customElement("ha-more-info-cover-favorite-positions")
|
||||
export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public stateObj!: CoverEntity;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@property({ attribute: false }) public editMode?: boolean;
|
||||
|
||||
@state() private _favoritePositions: number[] = [];
|
||||
|
||||
@state() private _favoriteTiltPositions: number[] = [];
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>): void {
|
||||
if (
|
||||
(changedProps.has("entry") || changedProps.has("stateObj")) &&
|
||||
this.entry &&
|
||||
this.stateObj
|
||||
) {
|
||||
const options = this.entry.options?.cover;
|
||||
|
||||
this._favoritePositions = coverSupportsPosition(this.stateObj)
|
||||
? normalizeCoverFavoritePositions(
|
||||
options?.favorite_positions ?? DEFAULT_COVER_FAVORITE_POSITIONS
|
||||
)
|
||||
: [];
|
||||
|
||||
this._favoriteTiltPositions = coverSupportsTiltPosition(this.stateObj)
|
||||
? normalizeCoverFavoritePositions(
|
||||
options?.favorite_tilt_positions ?? DEFAULT_COVER_FAVORITE_POSITIONS
|
||||
)
|
||||
: [];
|
||||
}
|
||||
}
|
||||
|
||||
private _localizeFavorite(
|
||||
kind: FavoriteKind,
|
||||
key: FavoriteLocalizeKey,
|
||||
values?: Record<string, string | number>
|
||||
): string {
|
||||
return this.hass.localize(
|
||||
`ui.dialogs.more_info_control.cover.${kind === "position" ? "favorite_position" : "favorite_tilt_position"}.${key}`,
|
||||
values
|
||||
);
|
||||
}
|
||||
|
||||
private _getFavorites(kind: FavoriteKind): number[] {
|
||||
return kind === "position"
|
||||
? this._favoritePositions
|
||||
: this._favoriteTiltPositions;
|
||||
}
|
||||
|
||||
private _getCurrentValue(kind: FavoriteKind): number | undefined {
|
||||
const current =
|
||||
kind === "position"
|
||||
? this.stateObj.attributes.current_position
|
||||
: this.stateObj.attributes.current_tilt_position;
|
||||
|
||||
return current == null ? undefined : Math.round(current);
|
||||
}
|
||||
|
||||
private async _save(options: CoverEntityOptions): Promise<void> {
|
||||
if (!this.entry) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentOptions: CoverEntityOptions = {
|
||||
...(this.entry.options?.cover ?? {}),
|
||||
};
|
||||
|
||||
if (coverSupportsPosition(this.stateObj)) {
|
||||
currentOptions.favorite_positions = this._favoritePositions;
|
||||
}
|
||||
|
||||
if (coverSupportsTiltPosition(this.stateObj)) {
|
||||
currentOptions.favorite_tilt_positions = this._favoriteTiltPositions;
|
||||
}
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this.entry.entity_id,
|
||||
{
|
||||
options_domain: "cover",
|
||||
options: {
|
||||
...currentOptions,
|
||||
...options,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
fireEvent(this, "entity-entry-updated", result.entity_entry);
|
||||
}
|
||||
|
||||
private async _setFavorites(
|
||||
kind: FavoriteKind,
|
||||
favorites: number[]
|
||||
): Promise<void> {
|
||||
const normalized = normalizeCoverFavoritePositions(favorites);
|
||||
|
||||
if (kind === "position") {
|
||||
this._favoritePositions = normalized;
|
||||
await this._save({ favorite_positions: normalized });
|
||||
return;
|
||||
}
|
||||
|
||||
this._favoriteTiltPositions = normalized;
|
||||
await this._save({ favorite_tilt_positions: normalized });
|
||||
}
|
||||
|
||||
private _move(kind: FavoriteKind, index: number, newIndex: number): void {
|
||||
const favorites = this._getFavorites(kind).concat();
|
||||
const moved = favorites.splice(index, 1)[0];
|
||||
favorites.splice(newIndex, 0, moved);
|
||||
this._setFavorites(kind, favorites);
|
||||
}
|
||||
|
||||
private _applyFavorite(kind: FavoriteKind, index: number): void {
|
||||
const favorite = this._getFavorites(kind)[index];
|
||||
|
||||
if (favorite === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (kind === "position") {
|
||||
this.hass.callService("cover", "set_cover_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
position: favorite,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.hass.callService("cover", "set_cover_tilt_position", {
|
||||
entity_id: this.stateObj.entity_id,
|
||||
tilt_position: favorite,
|
||||
});
|
||||
}
|
||||
|
||||
private async _promptFavoriteValue(
|
||||
kind: FavoriteKind,
|
||||
value?: number
|
||||
): Promise<number | undefined> {
|
||||
const response = await showPromptDialog(this, {
|
||||
title: this._localizeFavorite(
|
||||
kind,
|
||||
value === undefined ? "add_title" : "edit_title"
|
||||
),
|
||||
inputLabel: this.hass.localize(
|
||||
kind === "position"
|
||||
? "ui.card.cover.position"
|
||||
: "ui.card.cover.tilt_position"
|
||||
),
|
||||
inputType: "number",
|
||||
inputMin: "0",
|
||||
inputMax: "100",
|
||||
inputSuffix: DOMAIN_ATTRIBUTES_UNITS.cover.current_position,
|
||||
defaultValue: value === undefined ? undefined : String(value),
|
||||
});
|
||||
|
||||
if (response === null || response.trim() === "") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const number = Number(response);
|
||||
|
||||
if (isNaN(number)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Math.max(0, Math.min(100, Math.round(number)));
|
||||
}
|
||||
|
||||
private async _addFavorite(kind: FavoriteKind): Promise<void> {
|
||||
const value = await this._promptFavoriteValue(kind);
|
||||
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._setFavorites(kind, [...this._getFavorites(kind), value]);
|
||||
}
|
||||
|
||||
private async _editFavorite(
|
||||
kind: FavoriteKind,
|
||||
index: number
|
||||
): Promise<void> {
|
||||
const favorites = this._getFavorites(kind);
|
||||
const current = favorites[index];
|
||||
|
||||
if (current === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = await this._promptFavoriteValue(kind, current);
|
||||
|
||||
if (value === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = [...favorites];
|
||||
updated[index] = value;
|
||||
await this._setFavorites(kind, updated);
|
||||
}
|
||||
|
||||
private async _deleteFavorite(
|
||||
kind: FavoriteKind,
|
||||
index: number
|
||||
): Promise<void> {
|
||||
const confirmed = await showConfirmationDialog(this, {
|
||||
destructive: true,
|
||||
title: this._localizeFavorite(kind, "delete_confirm_title"),
|
||||
text: this._localizeFavorite(kind, "delete_confirm_text"),
|
||||
confirmText: this._localizeFavorite(kind, "delete_confirm_action"),
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this._setFavorites(
|
||||
kind,
|
||||
this._getFavorites(kind).filter((_, itemIndex) => itemIndex !== index)
|
||||
);
|
||||
}
|
||||
|
||||
private _renderFavoriteButton =
|
||||
(kind: FavoriteKind): HaMoreInfoFavorites["renderItem"] =>
|
||||
(favorite, _index, editMode) => {
|
||||
const currentValue = this._getCurrentValue(kind);
|
||||
const active = currentValue === favorite;
|
||||
const label = this._localizeFavorite(kind, editMode ? "edit" : "set", {
|
||||
value: `${favorite as number}%`,
|
||||
});
|
||||
|
||||
return html`
|
||||
<ha-control-button
|
||||
class=${classMap({
|
||||
active,
|
||||
})}
|
||||
style=${styleMap({
|
||||
"--control-button-border-radius": "var(--ha-border-radius-pill)",
|
||||
width: "72px",
|
||||
height: "36px",
|
||||
})}
|
||||
.label=${label}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
>
|
||||
${favorite as number}%
|
||||
</ha-control-button>
|
||||
`;
|
||||
};
|
||||
|
||||
private _deleteLabel =
|
||||
(kind: FavoriteKind): HaMoreInfoFavorites["deleteLabel"] =>
|
||||
(index) =>
|
||||
this._localizeFavorite(kind, "delete", {
|
||||
number: index + 1,
|
||||
});
|
||||
|
||||
private _handleFavoriteAction = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-action"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
const kind = favoriteKindFromEvent(ev);
|
||||
|
||||
const { action, index } = ev.detail;
|
||||
|
||||
if (action === "hold" && this.hass.user?.is_admin) {
|
||||
fireEvent(this, "toggle-edit-mode", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.editMode) {
|
||||
this._editFavorite(kind, index);
|
||||
return;
|
||||
}
|
||||
|
||||
this._applyFavorite(kind, index);
|
||||
};
|
||||
|
||||
private _handleFavoriteMoved = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-moved"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
const kind = favoriteKindFromEvent(ev);
|
||||
this._move(kind, ev.detail.oldIndex, ev.detail.newIndex);
|
||||
};
|
||||
|
||||
private _handleFavoriteDelete = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-delete"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
const kind = favoriteKindFromEvent(ev);
|
||||
this._deleteFavorite(kind, ev.detail.index);
|
||||
};
|
||||
|
||||
private _handleFavoriteAdd = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-add"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
const kind = favoriteKindFromEvent(ev);
|
||||
this._addFavorite(kind);
|
||||
};
|
||||
|
||||
private _handleFavoriteDone = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-done"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "toggle-edit-mode", false);
|
||||
};
|
||||
|
||||
private _renderKindSection(
|
||||
kind: FavoriteKind,
|
||||
label: string,
|
||||
favorites: number[],
|
||||
showDone: boolean,
|
||||
showLabel: boolean
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!this.editMode && favorites.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<section class="group">
|
||||
${showLabel ? html`<h4>${label}</h4>` : nothing}
|
||||
<ha-more-info-favorites
|
||||
data-kind=${kind}
|
||||
.items=${favorites}
|
||||
.renderItem=${this._renderFavoriteButton(kind)}
|
||||
.deleteLabel=${this._deleteLabel(kind)}
|
||||
.editMode=${this.editMode ?? false}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.isAdmin=${Boolean(this.hass.user?.is_admin)}
|
||||
.showDone=${showDone}
|
||||
.addLabel=${this._localizeFavorite(kind, "add")}
|
||||
.doneLabel=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.exit_edit_mode"
|
||||
)}
|
||||
@favorite-item-action=${this._handleFavoriteAction}
|
||||
@favorite-item-moved=${this._handleFavoriteMoved}
|
||||
@favorite-item-delete=${this._handleFavoriteDelete}
|
||||
@favorite-item-add=${this._handleFavoriteAdd}
|
||||
@favorite-item-done=${this._handleFavoriteDone}
|
||||
></ha-more-info-favorites>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this.stateObj || !this.entry) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportsPosition = coverSupportsPosition(this.stateObj);
|
||||
const supportsTiltPosition = coverSupportsTiltPosition(this.stateObj);
|
||||
const showPositionSection = supportsPosition
|
||||
? this.editMode || this._favoritePositions.length > 0
|
||||
: false;
|
||||
const showTiltSection = supportsTiltPosition
|
||||
? this.editMode || this._favoriteTiltPositions.length > 0
|
||||
: false;
|
||||
const showLabels =
|
||||
[showPositionSection, showTiltSection].filter(Boolean).length > 1;
|
||||
|
||||
const showDoneOnPosition = supportsPosition && !supportsTiltPosition;
|
||||
|
||||
return html`
|
||||
<div class="groups">
|
||||
${supportsPosition
|
||||
? this._renderKindSection(
|
||||
"position",
|
||||
this.hass.localize("ui.card.cover.position"),
|
||||
this._favoritePositions,
|
||||
showDoneOnPosition,
|
||||
showLabels
|
||||
)
|
||||
: nothing}
|
||||
${supportsTiltPosition
|
||||
? this._renderKindSection(
|
||||
"tilt",
|
||||
this.hass.localize("ui.card.cover.tilt_position"),
|
||||
this._favoriteTiltPositions,
|
||||
true,
|
||||
showLabels
|
||||
)
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
|
||||
.group {
|
||||
width: 100%;
|
||||
max-width: 384px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.group ha-more-info-favorites {
|
||||
--favorite-items-max-width: 384px;
|
||||
--favorite-item-active-background-color: var(--state-cover-active-color);
|
||||
}
|
||||
|
||||
h4 {
|
||||
margin: 0 0 var(--ha-space-2);
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
text-align: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-cover-favorite-positions": HaMoreInfoCoverFavoritePositions;
|
||||
}
|
||||
}
|
||||
267
src/dialogs/more-info/components/ha-more-info-favorites.ts
Normal file
267
src/dialogs/more-info/components/ha-more-info-favorites.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
import { mdiCheck, mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import "../../../components/ha-outlined-icon-button";
|
||||
import "../../../components/ha-sortable";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { actionHandler } from "../../../panels/lovelace/common/directives/action-handler-directive";
|
||||
|
||||
const SORTABLE_OPTIONS = {
|
||||
delay: 250,
|
||||
delayOnTouchOnly: true,
|
||||
};
|
||||
|
||||
@customElement("ha-more-info-favorites")
|
||||
export class HaMoreInfoFavorites extends LitElement {
|
||||
@property({ attribute: false }) public items: unknown[] = [];
|
||||
|
||||
@property({ attribute: false })
|
||||
public renderItem?: (
|
||||
item: unknown,
|
||||
index: number,
|
||||
editMode: boolean
|
||||
) => TemplateResult;
|
||||
|
||||
@property({ attribute: false })
|
||||
public deleteLabel?: (index: number) => string;
|
||||
|
||||
@property({ type: Boolean, attribute: false }) public editMode = false;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@property({ type: Boolean, attribute: false }) public isAdmin = false;
|
||||
|
||||
@property({ type: Boolean, attribute: false }) public showAdd = true;
|
||||
|
||||
@property({ type: Boolean, attribute: false }) public showDone = true;
|
||||
|
||||
@property({ attribute: false }) public addLabel = "";
|
||||
|
||||
@property({ attribute: false }) public doneLabel = "";
|
||||
|
||||
private _itemMoved(ev: HASSDomEvent<HASSDomEvents["item-moved"]>): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "favorite-item-moved", ev.detail);
|
||||
}
|
||||
|
||||
private _handleItemAction = (
|
||||
ev: HASSDomEvent<HASSDomEvents["action"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
fireEvent(this, "favorite-item-action", {
|
||||
index: Number(target.dataset.index),
|
||||
action: ev.detail.action,
|
||||
});
|
||||
};
|
||||
|
||||
private _handleDelete = (ev: MouseEvent): void => {
|
||||
ev.stopPropagation();
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
fireEvent(this, "favorite-item-delete", {
|
||||
index: Number(target.dataset.index),
|
||||
});
|
||||
};
|
||||
|
||||
private _handleAdd = (ev: MouseEvent): void => {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "favorite-item-add");
|
||||
};
|
||||
|
||||
private _handleDone = (ev: MouseEvent): void => {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "favorite-item-done");
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-sortable
|
||||
@item-moved=${this._itemMoved}
|
||||
draggable-selector=".favorite"
|
||||
no-style
|
||||
.disabled=${!this.editMode}
|
||||
.options=${SORTABLE_OPTIONS}
|
||||
>
|
||||
<div class="container">
|
||||
${this.items.map(
|
||||
(item, index) => html`
|
||||
<div class="favorite">
|
||||
<div
|
||||
class="favorite-bubble ${classMap({
|
||||
shake: !!this.editMode,
|
||||
})}"
|
||||
>
|
||||
<div
|
||||
class="item"
|
||||
data-index=${String(index)}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this.editMode && this.isAdmin,
|
||||
disabled: this.disabled,
|
||||
})}
|
||||
@action=${this._handleItemAction}
|
||||
>
|
||||
${this.renderItem
|
||||
? this.renderItem(item, index, this.editMode)
|
||||
: nothing}
|
||||
</div>
|
||||
${this.editMode
|
||||
? html`
|
||||
<button
|
||||
@click=${this._handleDelete}
|
||||
class="delete"
|
||||
data-index=${String(index)}
|
||||
aria-label=${ifDefined(this.deleteLabel?.(index))}
|
||||
title=${ifDefined(this.deleteLabel?.(index))}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiMinus}></ha-svg-icon>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.editMode && this.showAdd
|
||||
? html`
|
||||
<ha-outlined-icon-button
|
||||
class="button"
|
||||
@click=${this._handleAdd}
|
||||
.label=${this.addLabel}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
${this.editMode && this.showDone
|
||||
? html`
|
||||
<ha-outlined-icon-button
|
||||
@click=${this._handleDone}
|
||||
class="button"
|
||||
.label=${this.doneLabel}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: calc(var(--ha-space-2) * -1);
|
||||
flex-wrap: wrap;
|
||||
max-width: var(--favorite-items-max-width, 250px);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.favorite {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.favorite .favorite-bubble.shake {
|
||||
position: relative;
|
||||
display: block;
|
||||
animation: shake 0.45s linear infinite;
|
||||
}
|
||||
|
||||
.favorite:nth-child(3n + 1) .favorite-bubble.shake {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
|
||||
.favorite:nth-child(3n + 2) .favorite-bubble.shake {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform: rotateZ(0deg) translateX(-1px) translateY(0) scale(1);
|
||||
}
|
||||
20% {
|
||||
transform: rotateZ(-3deg) translateX(0) translateY(0);
|
||||
}
|
||||
40% {
|
||||
transform: rotateZ(0deg) translateX(1px) translateY(0);
|
||||
}
|
||||
60% {
|
||||
transform: rotateZ(3deg) translateX(0) translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotateZ(0deg) translateX(-1px) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.delete {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
inset-inline-end: -6px;
|
||||
inset-inline-start: initial;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
outline: none;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 0;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
--mdc-icon-size: 12px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.delete * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
ha-control-button.active {
|
||||
--control-button-background-color: var(
|
||||
--favorite-item-active-background-color
|
||||
);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
"favorite-item-action": {
|
||||
index: number;
|
||||
action: string;
|
||||
};
|
||||
"favorite-item-delete": {
|
||||
index: number;
|
||||
};
|
||||
"favorite-item-add";
|
||||
"favorite-item-done";
|
||||
"favorite-item-moved": {
|
||||
oldIndex: number;
|
||||
newIndex: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-favorites": HaMoreInfoFavorites;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,17 @@
|
||||
import { mdiCheck, mdiMinus, mdiPlus } from "@mdi/js";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { LitElement, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-control-slider";
|
||||
import "../../../../components/ha-sortable";
|
||||
import { UNAVAILABLE } from "../../../../data/entity/entity";
|
||||
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
|
||||
import type { LightColor, LightEntity } from "../../../../data/light";
|
||||
import { computeDefaultFavoriteColors } from "../../../../data/light";
|
||||
import { actionHandler } from "../../../../panels/lovelace/common/directives/action-handler-directive";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../generic/show-dialog-box";
|
||||
import "../ha-more-info-favorites";
|
||||
import type { HaMoreInfoFavorites } from "../ha-more-info-favorites";
|
||||
import "./ha-favorite-color-button";
|
||||
import { showLightColorFavoriteDialog } from "./show-dialog-light-color-favorite";
|
||||
|
||||
@@ -36,40 +34,32 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
@state() private _favoriteColors: LightColor[] = [];
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
if (changedProps.has("entry")) {
|
||||
if (this.entry) {
|
||||
if (this.entry.options?.light?.favorite_colors) {
|
||||
this._favoriteColors = this.entry.options.light.favorite_colors;
|
||||
} else if (this.stateObj) {
|
||||
this._favoriteColors = computeDefaultFavoriteColors(this.stateObj);
|
||||
}
|
||||
if (changedProps.has("entry") && this.entry) {
|
||||
if (this.entry.options?.light?.favorite_colors) {
|
||||
this._favoriteColors = this.entry.options.light.favorite_colors;
|
||||
} else if (this.stateObj) {
|
||||
this._favoriteColors = computeDefaultFavoriteColors(this.stateObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _colorMoved(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const { oldIndex, newIndex } = ev.detail;
|
||||
this._move(oldIndex, newIndex);
|
||||
}
|
||||
|
||||
private _move(index: number, newIndex: number) {
|
||||
private _move(index: number, newIndex: number): void {
|
||||
const favoriteColors = this._favoriteColors.concat();
|
||||
const action = favoriteColors.splice(index, 1)[0];
|
||||
favoriteColors.splice(newIndex, 0, action);
|
||||
const color = favoriteColors.splice(index, 1)[0];
|
||||
favoriteColors.splice(newIndex, 0, color);
|
||||
this._favoriteColors = favoriteColors;
|
||||
this._save(favoriteColors);
|
||||
}
|
||||
|
||||
private _apply = (index: number) => {
|
||||
private _apply(index: number): void {
|
||||
const favorite = this._favoriteColors[index];
|
||||
this.hass.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
entity_id: this.stateObj.entity_id,
|
||||
...favorite,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
private async _save(newFavoriteColors: LightColor[]) {
|
||||
private async _save(newFavoriteColors: LightColor[]): Promise<void> {
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this.entry!.entity_id,
|
||||
@@ -83,23 +73,23 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
fireEvent(this, "entity-entry-updated", result.entity_entry);
|
||||
}
|
||||
|
||||
private _add = async () => {
|
||||
private _add = async (): Promise<void> => {
|
||||
const color = await showLightColorFavoriteDialog(this, {
|
||||
entry: this.entry!,
|
||||
title: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.add_title"
|
||||
),
|
||||
});
|
||||
if (color) {
|
||||
const newFavoriteColors = [...this._favoriteColors, color];
|
||||
this._save(newFavoriteColors);
|
||||
if (!color) {
|
||||
return;
|
||||
}
|
||||
const newFavoriteColors = [...this._favoriteColors, color];
|
||||
this._save(newFavoriteColors);
|
||||
};
|
||||
|
||||
private _edit = async (index) => {
|
||||
// Make sure the current favorite color is set
|
||||
private _edit = async (index: number): Promise<void> => {
|
||||
fireEvent(this, "favorite-color-edit-started");
|
||||
await this._apply(index);
|
||||
this._apply(index);
|
||||
const color = await showLightColorFavoriteDialog(this, {
|
||||
entry: this.entry!,
|
||||
initialColor: this._favoriteColors[index],
|
||||
@@ -108,26 +98,27 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
),
|
||||
});
|
||||
|
||||
if (color) {
|
||||
const newFavoriteColors = [...this._favoriteColors];
|
||||
newFavoriteColors[index] = color;
|
||||
this._save(newFavoriteColors);
|
||||
} else {
|
||||
if (!color) {
|
||||
this._apply(index);
|
||||
return;
|
||||
}
|
||||
|
||||
const newFavoriteColors = [...this._favoriteColors];
|
||||
newFavoriteColors[index] = color;
|
||||
this._save(newFavoriteColors);
|
||||
};
|
||||
|
||||
private _delete = async (index) => {
|
||||
private _delete = async (index: number): Promise<void> => {
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
destructive: true,
|
||||
title: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.delete_confirm_title`
|
||||
"ui.dialogs.more_info_control.light.favorite_color.delete_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.delete_confirm_text`
|
||||
"ui.dialogs.more_info_control.light.favorite_color.delete_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.delete_confirm_action`
|
||||
"ui.dialogs.more_info_control.light.favorite_color.delete_confirm_action"
|
||||
),
|
||||
});
|
||||
if (!confirm) {
|
||||
@@ -139,196 +130,101 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
this._save(newFavoriteColors);
|
||||
};
|
||||
|
||||
private _handleDeleteButton = (ev) => {
|
||||
ev.stopPropagation();
|
||||
const index = ev.target.index;
|
||||
this._delete(index);
|
||||
};
|
||||
private _renderFavorite = (
|
||||
color: LightColor,
|
||||
index: number,
|
||||
editMode: boolean
|
||||
): TemplateResult =>
|
||||
html`<ha-favorite-color-button
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.${
|
||||
editMode ? "edit" : "set"
|
||||
}`,
|
||||
{ number: index + 1 }
|
||||
)}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.color=${color}
|
||||
></ha-favorite-color-button>`;
|
||||
|
||||
private _handleAddButton = (ev) => {
|
||||
ev.stopPropagation();
|
||||
this._add();
|
||||
};
|
||||
private _deleteLabel = (index: number): string =>
|
||||
this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.delete",
|
||||
{
|
||||
number: index + 1,
|
||||
}
|
||||
);
|
||||
|
||||
private _handleColorAction = (ev) => {
|
||||
private _handleFavoriteAction = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-action"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.action === "hold" && this.hass.user?.is_admin) {
|
||||
|
||||
const { action, index } = ev.detail;
|
||||
|
||||
if (action === "hold" && this.hass.user?.is_admin) {
|
||||
fireEvent(this, "toggle-edit-mode", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const index = ev.target.index;
|
||||
if (this.editMode) {
|
||||
this._edit(index);
|
||||
return;
|
||||
}
|
||||
|
||||
this._apply(index);
|
||||
};
|
||||
|
||||
private _exitEditMode = (ev) => {
|
||||
private _handleFavoriteMoved = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-moved"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
this._move(ev.detail.oldIndex, ev.detail.newIndex);
|
||||
};
|
||||
|
||||
private _handleFavoriteDelete = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-delete"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
this._delete(ev.detail.index);
|
||||
};
|
||||
|
||||
private _handleFavoriteAdd = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-add"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
this._add();
|
||||
};
|
||||
|
||||
private _handleFavoriteDone = (
|
||||
ev: HASSDomEvent<HASSDomEvents["favorite-item-done"]>
|
||||
): void => {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "toggle-edit-mode", false);
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-sortable
|
||||
@item-moved=${this._colorMoved}
|
||||
draggable-selector=".color"
|
||||
no-style
|
||||
.disabled=${!this.editMode}
|
||||
>
|
||||
<div class="container">
|
||||
${this._favoriteColors.map(
|
||||
(color, index) => html`
|
||||
<div class="color">
|
||||
<div
|
||||
class="color-bubble ${classMap({
|
||||
shake: !!this.editMode,
|
||||
})}"
|
||||
>
|
||||
<ha-favorite-color-button
|
||||
.label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.${
|
||||
this.editMode ? "edit" : "set"
|
||||
}`,
|
||||
{ number: index }
|
||||
)}
|
||||
.disabled=${this.stateObj!.state === UNAVAILABLE}
|
||||
.color=${color}
|
||||
.index=${index}
|
||||
.actionHandler=${actionHandler({
|
||||
hasHold: !this.editMode && this.hass.user?.is_admin,
|
||||
disabled: this.stateObj!.state === UNAVAILABLE,
|
||||
})}
|
||||
@action=${this._handleColorAction}
|
||||
>
|
||||
</ha-favorite-color-button>
|
||||
${this.editMode
|
||||
? html`
|
||||
<button
|
||||
@click=${this._handleDeleteButton}
|
||||
class="delete"
|
||||
.index=${index}
|
||||
aria-label=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
||||
{ number: index }
|
||||
)}
|
||||
.title=${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.favorite_color.delete`,
|
||||
{ number: index }
|
||||
)}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiMinus}></ha-svg-icon>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${this.editMode
|
||||
? html`
|
||||
<ha-outlined-icon-button
|
||||
class="button"
|
||||
@click=${this._handleAddButton}
|
||||
>
|
||||
<ha-svg-icon .path=${mdiPlus}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
<ha-outlined-icon-button
|
||||
@click=${this._exitEditMode}
|
||||
class="button"
|
||||
>
|
||||
<ha-svg-icon .path=${mdiCheck}></ha-svg-icon>
|
||||
</ha-outlined-icon-button>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-sortable>
|
||||
<ha-more-info-favorites
|
||||
.items=${this._favoriteColors}
|
||||
.renderItem=${this._renderFavorite as HaMoreInfoFavorites["renderItem"]}
|
||||
.deleteLabel=${this._deleteLabel as HaMoreInfoFavorites["deleteLabel"]}
|
||||
.editMode=${this.editMode}
|
||||
.disabled=${this.stateObj.state === UNAVAILABLE}
|
||||
.isAdmin=${Boolean(this.hass.user?.is_admin)}
|
||||
.addLabel=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.favorite_color.add"
|
||||
)}
|
||||
.doneLabel=${this.hass.localize(
|
||||
"ui.dialogs.more_info_control.exit_edit_mode"
|
||||
)}
|
||||
@favorite-item-action=${this._handleFavoriteAction}
|
||||
@favorite-item-moved=${this._handleFavoriteMoved}
|
||||
@favorite-item-delete=${this._handleFavoriteDelete}
|
||||
@favorite-item-add=${this._handleFavoriteAdd}
|
||||
@favorite-item-done=${this._handleFavoriteDone}
|
||||
></ha-more-info-favorites>
|
||||
`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: calc(var(--ha-space-2) * -1);
|
||||
flex-wrap: wrap;
|
||||
max-width: 250px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.container > * {
|
||||
margin: var(--ha-space-2);
|
||||
}
|
||||
|
||||
.color {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.color .color-bubble.shake {
|
||||
position: relative;
|
||||
display: block;
|
||||
animation: shake 0.45s linear infinite;
|
||||
}
|
||||
.color:nth-child(3n + 1) .color-bubble.shake {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
.color:nth-child(3n + 2) .color-bubble.shake {
|
||||
animation-delay: 0.3s;
|
||||
}
|
||||
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.sortable-fallback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0% {
|
||||
transform: rotateZ(0deg) translateX(-1px) translateY(0) scale(1);
|
||||
}
|
||||
20% {
|
||||
transform: rotateZ(-3deg) translateX(0) translateY();
|
||||
}
|
||||
40% {
|
||||
transform: rotateZ(0deg) translateX(1px) translateY(0);
|
||||
}
|
||||
60% {
|
||||
transform: rotateZ(3deg) translateX(0) translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: rotateZ(0deg) translateX(-1px) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.delete {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
inset-inline-end: -6px;
|
||||
inset-inline-start: initial;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
outline: none;
|
||||
background-color: var(--secondary-background-color);
|
||||
padding: 0;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
.delete {
|
||||
--mdc-icon-size: 12px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
.delete * {
|
||||
pointer-events: none;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -5,9 +5,16 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-icon-button-group";
|
||||
import "../../../components/ha-icon-button-toggle";
|
||||
import {
|
||||
shouldShowFavoriteOptions,
|
||||
type ExtEntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import type { CoverEntity } from "../../../data/cover";
|
||||
import {
|
||||
CoverEntityFeature,
|
||||
coverSupportsAnyPosition,
|
||||
coverSupportsPosition,
|
||||
coverSupportsTiltPosition,
|
||||
computeCoverPositionStateDisplay,
|
||||
} from "../../../data/cover";
|
||||
import "../../../state-control/cover/ha-state-control-cover-buttons";
|
||||
@@ -15,6 +22,7 @@ import "../../../state-control/cover/ha-state-control-cover-position";
|
||||
import "../../../state-control/cover/ha-state-control-cover-tilt-position";
|
||||
import "../../../state-control/cover/ha-state-control-cover-toggle";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../components/covers/ha-more-info-cover-favorite-positions";
|
||||
import "../components/ha-more-info-state-header";
|
||||
import { moreInfoControlStyle } from "../components/more-info-control-style";
|
||||
|
||||
@@ -26,6 +34,10 @@ class MoreInfoCover extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public stateObj?: CoverEntity;
|
||||
|
||||
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@property({ attribute: false }) public editMode?: boolean;
|
||||
|
||||
@state() private _mode?: Mode;
|
||||
|
||||
private _setMode(ev) {
|
||||
@@ -38,11 +50,9 @@ class MoreInfoCover extends LitElement {
|
||||
const entityId = this.stateObj.entity_id;
|
||||
const oldEntityId = changedProps.get("stateObj")?.entity_id;
|
||||
if (!this._mode || entityId !== oldEntityId) {
|
||||
this._mode =
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.SET_POSITION) ||
|
||||
supportsFeature(this.stateObj, CoverEntityFeature.SET_TILT_POSITION)
|
||||
? "position"
|
||||
: "button";
|
||||
this._mode = coverSupportsAnyPosition(this.stateObj)
|
||||
? "position"
|
||||
: "button";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,14 +76,21 @@ class MoreInfoCover extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const supportsPosition = supportsFeature(
|
||||
this.stateObj,
|
||||
CoverEntityFeature.SET_POSITION
|
||||
);
|
||||
const supportsPosition = coverSupportsPosition(this.stateObj);
|
||||
|
||||
const supportsTiltPosition = supportsFeature(
|
||||
this.stateObj,
|
||||
CoverEntityFeature.SET_TILT_POSITION
|
||||
const supportsTiltPosition = coverSupportsTiltPosition(this.stateObj);
|
||||
|
||||
const showFavoriteControls = Boolean(
|
||||
this.entry &&
|
||||
(this.editMode ||
|
||||
(coverSupportsPosition(this.stateObj) &&
|
||||
shouldShowFavoriteOptions(
|
||||
this.entry.options?.cover?.favorite_positions
|
||||
)) ||
|
||||
(coverSupportsTiltPosition(this.stateObj) &&
|
||||
shouldShowFavoriteOptions(
|
||||
this.entry.options?.cover?.favorite_tilt_positions
|
||||
)))
|
||||
);
|
||||
|
||||
const supportsOpenClose =
|
||||
@@ -174,6 +191,18 @@ class MoreInfoCover extends LitElement {
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
${
|
||||
showFavoriteControls
|
||||
? html`
|
||||
<ha-more-info-cover-favorite-positions
|
||||
.hass=${this.hass}
|
||||
.stateObj=${this.stateObj}
|
||||
.entry=${this.entry}
|
||||
.editMode=${this.editMode}
|
||||
></ha-more-info-cover-favorite-positions>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -16,7 +16,10 @@ import "../../../components/ha-icon-button-group";
|
||||
import "../../../components/ha-icon-button-toggle";
|
||||
import "../../../components/ha-list-item";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
shouldShowFavoriteOptions,
|
||||
type ExtEntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { forwardHaptic } from "../../../data/haptics";
|
||||
import type { LightEntity } from "../../../data/light";
|
||||
import {
|
||||
@@ -110,10 +113,14 @@ class MoreInfoLight extends LitElement {
|
||||
LightEntityFeature.EFFECT
|
||||
);
|
||||
|
||||
const hasFavoriteColors =
|
||||
const showFavoriteColors = Boolean(
|
||||
this.entry &&
|
||||
(this.entry.options?.light?.favorite_colors == null ||
|
||||
this.entry.options.light.favorite_colors.length > 0);
|
||||
(this.editMode ||
|
||||
(lightSupportsFavoriteColors(this.stateObj) &&
|
||||
shouldShowFavoriteOptions(
|
||||
this.entry.options?.light?.favorite_colors
|
||||
)))
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-more-info-state-header
|
||||
@@ -239,9 +246,7 @@ class MoreInfoLight extends LitElement {
|
||||
`
|
||||
: nothing}
|
||||
</ha-icon-button-group>
|
||||
${this.entry &&
|
||||
lightSupportsFavoriteColors(this.stateObj) &&
|
||||
(this.editMode || hasFavoriteColors)
|
||||
${showFavoriteColors
|
||||
? html`
|
||||
<ha-more-info-light-favorite-colors
|
||||
.hass=${this.hass}
|
||||
|
||||
289
src/dialogs/more-info/favorites.ts
Normal file
289
src/dialogs/more-info/favorites.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
import type { HassEntity } from "home-assistant-js-websocket";
|
||||
import { html, type LitElement } from "lit";
|
||||
import {
|
||||
hasRejectedItems,
|
||||
rejectedItems,
|
||||
} from "../../common/util/promise-all-settled-results";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import type { CoverEntity } from "../../data/cover";
|
||||
import {
|
||||
DEFAULT_COVER_FAVORITE_POSITIONS,
|
||||
coverSupportsAnyPosition,
|
||||
coverSupportsPosition,
|
||||
coverSupportsTiltPosition,
|
||||
normalizeCoverFavoritePositions,
|
||||
} from "../../data/cover";
|
||||
import type {
|
||||
ExtEntityRegistryEntry,
|
||||
FavoriteOption,
|
||||
FavoritesDomain,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import {
|
||||
hasCustomFavoriteOptionValues,
|
||||
isFavoritesDomain,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { LightColor, LightEntity } from "../../data/light";
|
||||
import {
|
||||
LightColorMode,
|
||||
computeDefaultFavoriteColors,
|
||||
lightSupportsColor,
|
||||
lightSupportsColorMode,
|
||||
lightSupportsFavoriteColors,
|
||||
} from "../../data/light";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showAlertDialog } from "../generic/show-dialog-box";
|
||||
import { showFormDialog } from "../form/show-form-dialog";
|
||||
|
||||
export interface FavoritesDialogContext {
|
||||
host: LitElement;
|
||||
hass: HomeAssistant;
|
||||
entry: ExtEntityRegistryEntry;
|
||||
stateObj: HassEntity;
|
||||
}
|
||||
|
||||
interface FavoritesDialogLabels {
|
||||
editMode: string;
|
||||
reset: string;
|
||||
resetText: string;
|
||||
copy: string;
|
||||
}
|
||||
|
||||
export interface FavoritesDialogHandler {
|
||||
domain: FavoritesDomain;
|
||||
supports: (stateObj: HassEntity) => boolean;
|
||||
hasCustomFavorites: (entry: ExtEntityRegistryEntry) => boolean;
|
||||
getResetOptions: (
|
||||
stateObj: HassEntity
|
||||
) => Partial<Record<FavoriteOption, undefined>>;
|
||||
getLabels: (hass: HomeAssistant) => FavoritesDialogLabels;
|
||||
copy: (ctx: FavoritesDialogContext) => Promise<void>;
|
||||
}
|
||||
|
||||
const getFavoritesDialogLabels = (
|
||||
hass: HomeAssistant,
|
||||
domain: FavoritesDomain
|
||||
): FavoritesDialogLabels => ({
|
||||
editMode: hass.localize(`ui.dialogs.more_info_control.${domain}.edit_mode`),
|
||||
reset: hass.localize(
|
||||
`ui.dialogs.more_info_control.${domain}.reset_favorites`
|
||||
),
|
||||
resetText: hass.localize(
|
||||
`ui.dialogs.more_info_control.${domain}.reset_favorites_text`
|
||||
),
|
||||
copy: hass.localize(`ui.dialogs.more_info_control.${domain}.copy_favorites`),
|
||||
});
|
||||
|
||||
const copyFavoriteOptionsToEntities = async (
|
||||
host: LitElement,
|
||||
hass: HomeAssistant,
|
||||
domain: FavoritesDomain,
|
||||
includeEntities: string[],
|
||||
options: object
|
||||
) => {
|
||||
const registryBackedEntities = includeEntities.filter(
|
||||
(entityId) => entityId in hass.entities
|
||||
);
|
||||
|
||||
const selected = await showFormDialog(host, {
|
||||
title: hass.localize(
|
||||
`ui.dialogs.more_info_control.${domain}.copy_favorites`
|
||||
),
|
||||
submitText: hass.localize("ui.common.copy"),
|
||||
schema: [
|
||||
{
|
||||
name: "entity",
|
||||
selector: {
|
||||
entity: {
|
||||
include_entities: registryBackedEntities,
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
computeLabel: () =>
|
||||
hass.localize(
|
||||
`ui.dialogs.more_info_control.${domain}.copy_favorites_entities`
|
||||
),
|
||||
computeHelper: () =>
|
||||
hass.localize(
|
||||
`ui.dialogs.more_info_control.${domain}.copy_favorites_helper`
|
||||
),
|
||||
data: {},
|
||||
});
|
||||
|
||||
if (selected?.entity) {
|
||||
const result = await Promise.allSettled(
|
||||
selected.entity.map((entityId: string) =>
|
||||
updateEntityRegistryEntry(hass, entityId, {
|
||||
options_domain: domain,
|
||||
options,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
if (hasRejectedItems(result)) {
|
||||
const rejected = rejectedItems(result);
|
||||
|
||||
showAlertDialog(host, {
|
||||
title: hass.localize("ui.panel.config.common.multiselect.failed", {
|
||||
number: rejected.length,
|
||||
}),
|
||||
text: html`<pre>
|
||||
${rejected
|
||||
.map(
|
||||
(item) => item.reason.message || item.reason.code || item.reason
|
||||
)
|
||||
.join("\r\n")}</pre
|
||||
>`,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const coverFavoritesHandler: FavoritesDialogHandler = {
|
||||
domain: "cover",
|
||||
supports: (stateObj) => coverSupportsAnyPosition(stateObj as CoverEntity),
|
||||
hasCustomFavorites: (entry) =>
|
||||
hasCustomFavoriteOptionValues(entry.options?.cover?.favorite_positions) ||
|
||||
hasCustomFavoriteOptionValues(
|
||||
entry.options?.cover?.favorite_tilt_positions
|
||||
),
|
||||
getResetOptions: (stateObj) => ({
|
||||
...(coverSupportsPosition(stateObj as CoverEntity)
|
||||
? { favorite_positions: undefined }
|
||||
: {}),
|
||||
...(coverSupportsTiltPosition(stateObj as CoverEntity)
|
||||
? { favorite_tilt_positions: undefined }
|
||||
: {}),
|
||||
}),
|
||||
getLabels: (hass) => getFavoritesDialogLabels(hass, "cover"),
|
||||
copy: async ({ entry, hass, host, stateObj }) => {
|
||||
const coverStateObj = stateObj as CoverEntity;
|
||||
|
||||
const favoritePositions = coverSupportsPosition(coverStateObj)
|
||||
? normalizeCoverFavoritePositions(
|
||||
entry.options?.cover?.favorite_positions ??
|
||||
DEFAULT_COVER_FAVORITE_POSITIONS
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const favoriteTiltPositions = coverSupportsTiltPosition(coverStateObj)
|
||||
? normalizeCoverFavoritePositions(
|
||||
entry.options?.cover?.favorite_tilt_positions ??
|
||||
DEFAULT_COVER_FAVORITE_POSITIONS
|
||||
)
|
||||
: undefined;
|
||||
|
||||
const compatibleCovers = Object.values(hass.states).filter((candidate) => {
|
||||
if (
|
||||
candidate.entity_id === coverStateObj.entity_id ||
|
||||
computeStateDomain(candidate) !== "cover"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(!coverSupportsPosition(coverStateObj) ||
|
||||
coverSupportsPosition(candidate as CoverEntity)) &&
|
||||
(!coverSupportsTiltPosition(coverStateObj) ||
|
||||
coverSupportsTiltPosition(candidate as CoverEntity))
|
||||
);
|
||||
});
|
||||
|
||||
await copyFavoriteOptionsToEntities(
|
||||
host,
|
||||
hass,
|
||||
"cover",
|
||||
compatibleCovers.map((cover) => cover.entity_id),
|
||||
{
|
||||
...(favoritePositions !== undefined
|
||||
? { favorite_positions: [...favoritePositions] }
|
||||
: {}),
|
||||
...(favoriteTiltPositions !== undefined
|
||||
? { favorite_tilt_positions: [...favoriteTiltPositions] }
|
||||
: {}),
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const lightFavoritesHandler: FavoritesDialogHandler = {
|
||||
domain: "light",
|
||||
supports: (stateObj) => lightSupportsFavoriteColors(stateObj as LightEntity),
|
||||
hasCustomFavorites: (entry) =>
|
||||
hasCustomFavoriteOptionValues(entry.options?.light?.favorite_colors),
|
||||
getResetOptions: () => ({
|
||||
favorite_colors: undefined,
|
||||
}),
|
||||
getLabels: (hass) => getFavoritesDialogLabels(hass, "light"),
|
||||
copy: async ({ entry, hass, host, stateObj }) => {
|
||||
const lightStateObj = stateObj as LightEntity;
|
||||
const favorites: LightColor[] =
|
||||
entry.options?.light?.favorite_colors ??
|
||||
computeDefaultFavoriteColors(lightStateObj);
|
||||
|
||||
const favoriteTypes = [
|
||||
...new Set(favorites.map((item) => Object.keys(item)[0])),
|
||||
];
|
||||
|
||||
const compatibleLights = Object.values(hass.states).filter(
|
||||
(candidate) =>
|
||||
candidate.entity_id !== lightStateObj.entity_id &&
|
||||
computeStateDomain(candidate) === "light" &&
|
||||
favoriteTypes.every((type) =>
|
||||
type === "color_temp_kelvin"
|
||||
? lightSupportsColorMode(
|
||||
candidate as LightEntity,
|
||||
LightColorMode.COLOR_TEMP
|
||||
)
|
||||
: type === "hs_color" || type === "rgb_color"
|
||||
? lightSupportsColor(candidate as LightEntity)
|
||||
: type === "rgbw_color"
|
||||
? lightSupportsColorMode(
|
||||
candidate as LightEntity,
|
||||
LightColorMode.RGBW
|
||||
)
|
||||
: type === "rgbww_color"
|
||||
? lightSupportsColorMode(
|
||||
candidate as LightEntity,
|
||||
LightColorMode.RGBWW
|
||||
)
|
||||
: false
|
||||
)
|
||||
);
|
||||
|
||||
await copyFavoriteOptionsToEntities(
|
||||
host,
|
||||
hass,
|
||||
"light",
|
||||
compatibleLights.map((light) => light.entity_id),
|
||||
{
|
||||
favorite_colors: favorites,
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const FAVORITES_DIALOG_HANDLERS: Record<
|
||||
FavoritesDomain,
|
||||
FavoritesDialogHandler
|
||||
> = {
|
||||
cover: coverFavoritesHandler,
|
||||
light: lightFavoritesHandler,
|
||||
};
|
||||
|
||||
export const getFavoritesDialogHandler = (
|
||||
stateObj: HassEntity
|
||||
): FavoritesDialogHandler | undefined => {
|
||||
const domain = computeStateDomain(stateObj);
|
||||
|
||||
if (!isFavoritesDomain(domain)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return FAVORITES_DIALOG_HANDLERS[domain].supports(stateObj)
|
||||
? FAVORITES_DIALOG_HANDLERS[domain]
|
||||
: undefined;
|
||||
};
|
||||
@@ -55,14 +55,6 @@ import {
|
||||
getExtendedEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../data/entity/entity_registry";
|
||||
import type { LightColor } from "../../data/light";
|
||||
import {
|
||||
LightColorMode,
|
||||
lightSupportsColor,
|
||||
computeDefaultFavoriteColors,
|
||||
lightSupportsColorMode,
|
||||
lightSupportsFavoriteColors,
|
||||
} from "../../data/light";
|
||||
import type { ItemType } from "../../data/search";
|
||||
import { SearchableDomains } from "../../data/search";
|
||||
import { getSensorNumericDeviceClasses } from "../../data/sensor";
|
||||
@@ -87,9 +79,9 @@ import "./ha-more-info-history-and-logbook";
|
||||
import "./ha-more-info-info";
|
||||
import "./ha-more-info-settings";
|
||||
import "./more-info-content";
|
||||
import type { FavoritesDialogContext } from "./favorites";
|
||||
import { getFavoritesDialogHandler } from "./favorites";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
||||
import { showFormDialog } from "../form/show-form-dialog";
|
||||
|
||||
export interface MoreInfoDialogParams {
|
||||
entityId: string | null;
|
||||
@@ -343,7 +335,9 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _toggleInfoEditMode() {
|
||||
this._infoEditMode = !this._infoEditMode;
|
||||
withViewTransition(() => {
|
||||
this._infoEditMode = !this._infoEditMode;
|
||||
});
|
||||
}
|
||||
|
||||
private _toggleDetailsYamlMode() {
|
||||
@@ -355,13 +349,32 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _handleToggleInfoEditModeEvent(ev) {
|
||||
this._infoEditMode = ev.detail;
|
||||
withViewTransition(() => {
|
||||
this._infoEditMode = ev.detail;
|
||||
});
|
||||
}
|
||||
|
||||
private _goToRelated(): void {
|
||||
this._setView("related");
|
||||
}
|
||||
|
||||
private _getFavoritesContext(): FavoritesDialogContext | undefined {
|
||||
const entityId = this._entityId;
|
||||
const stateObj =
|
||||
entityId && (this.hass.states[entityId] as HassEntity | undefined);
|
||||
|
||||
if (!this._entry || !stateObj) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
host: this,
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
stateObj,
|
||||
};
|
||||
}
|
||||
|
||||
private _handleMenuAction(ev: HaDropdownSelectEvent) {
|
||||
const action = ev.detail?.item?.value;
|
||||
switch (action) {
|
||||
@@ -396,16 +409,28 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _resetFavorites() {
|
||||
const favoritesContext = this._getFavoritesContext();
|
||||
|
||||
if (!favoritesContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
const favoritesHandler = getFavoritesDialogHandler(
|
||||
favoritesContext.stateObj
|
||||
);
|
||||
|
||||
if (!favoritesHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const labels = favoritesHandler.getLabels(this.hass);
|
||||
|
||||
if (
|
||||
!(await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.dialogs.more_info_control.light.reset_favorites"
|
||||
),
|
||||
text: this.hass!.localize(
|
||||
"ui.dialogs.more_info_control.light.reset_favorites_text"
|
||||
),
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
confirmText: this.hass!.localize("ui.common.reset"),
|
||||
title: labels.reset,
|
||||
text: labels.resetText,
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirmText: this.hass.localize("ui.common.reset"),
|
||||
destructive: true,
|
||||
}))
|
||||
) {
|
||||
@@ -414,12 +439,10 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this._entry!.entity_id,
|
||||
favoritesContext.entry.entity_id,
|
||||
{
|
||||
options_domain: "light",
|
||||
options: {
|
||||
favorite_colors: undefined,
|
||||
},
|
||||
options_domain: favoritesHandler.domain,
|
||||
options: favoritesHandler.getResetOptions(favoritesContext.stateObj),
|
||||
}
|
||||
);
|
||||
this._entry = result.entity_entry;
|
||||
@@ -435,76 +458,21 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private async _copyFavorites() {
|
||||
const entityId = this._entityId;
|
||||
const stateObj =
|
||||
entityId && (this.hass.states[entityId] as HassEntity | undefined);
|
||||
let favorites: LightColor[] | undefined;
|
||||
if (this._entry!.options?.light?.favorite_colors) {
|
||||
favorites = this._entry!.options.light.favorite_colors;
|
||||
} else if (stateObj) {
|
||||
favorites = computeDefaultFavoriteColors(stateObj);
|
||||
const favoritesContext = this._getFavoritesContext();
|
||||
|
||||
if (!favoritesContext) {
|
||||
return;
|
||||
}
|
||||
if (!favorites) return;
|
||||
|
||||
const favoriteTypes = [...new Set(favorites.map((o) => Object.keys(o)[0]))];
|
||||
|
||||
const compatibleLights = Object.values(this.hass.states).filter(
|
||||
(s) =>
|
||||
s.entity_id !== entityId &&
|
||||
computeStateDomain(s) === "light" &&
|
||||
favoriteTypes.every((type) =>
|
||||
type === "color_temp_kelvin"
|
||||
? lightSupportsColorMode(s, LightColorMode.COLOR_TEMP)
|
||||
: type === "hs_color" || type === "rgb_color"
|
||||
? lightSupportsColor(s)
|
||||
: type === "rgbw_color"
|
||||
? lightSupportsColorMode(s, LightColorMode.RGBW)
|
||||
: type === "rgbww_color"
|
||||
? lightSupportsColorMode(s, LightColorMode.RGBWW)
|
||||
: false
|
||||
)
|
||||
const favoritesHandler = getFavoritesDialogHandler(
|
||||
favoritesContext.stateObj
|
||||
);
|
||||
|
||||
const schema = [
|
||||
{
|
||||
name: "entity",
|
||||
selector: {
|
||||
entity: {
|
||||
include_entities: compatibleLights.map((l) => l.entity_id),
|
||||
multiple: true,
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
},
|
||||
];
|
||||
if (!favoritesHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const computeLabel = () =>
|
||||
this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.copy_favorites_entities"
|
||||
);
|
||||
const computeHelper = () =>
|
||||
this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.copy_favorites_helper"
|
||||
);
|
||||
|
||||
const selected = await showFormDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.dialogs.more_info_control.light.copy_favorites"
|
||||
),
|
||||
schema,
|
||||
computeLabel,
|
||||
computeHelper,
|
||||
data: {},
|
||||
});
|
||||
|
||||
selected?.entity.forEach((id) => {
|
||||
updateEntityRegistryEntry(this.hass, id, {
|
||||
options_domain: "light",
|
||||
options: {
|
||||
favorite_colors: favorites,
|
||||
},
|
||||
});
|
||||
});
|
||||
await favoritesHandler.copy(favoritesContext);
|
||||
}
|
||||
|
||||
private _goToAddEntityTo(ev) {
|
||||
@@ -582,6 +550,29 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
);
|
||||
const title = this._childView?.viewTitle || breadcrumb.pop() || entityId;
|
||||
|
||||
const favoritesContext =
|
||||
this._entry && stateObj
|
||||
? {
|
||||
host: this,
|
||||
hass: this.hass,
|
||||
entry: this._entry,
|
||||
stateObj,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const favoritesHandler = favoritesContext
|
||||
? getFavoritesDialogHandler(favoritesContext.stateObj)
|
||||
: undefined;
|
||||
|
||||
const favoritesLabels = favoritesHandler?.getLabels(this.hass);
|
||||
|
||||
const supportsFavorites = Boolean(favoritesHandler && favoritesContext);
|
||||
|
||||
const resetFavoritesDisabled =
|
||||
favoritesContext && favoritesHandler
|
||||
? !favoritesHandler.hasCustomFavorites(favoritesContext.entry)
|
||||
: false;
|
||||
|
||||
const isRTL = computeRTL(this.hass);
|
||||
|
||||
return html`
|
||||
@@ -665,6 +656,41 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${supportsFavorites
|
||||
? html`
|
||||
<ha-dropdown-item value="toggle_edit">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${this._infoEditMode
|
||||
? mdiPencilOff
|
||||
: mdiPencil}
|
||||
></ha-svg-icon>
|
||||
${this._infoEditMode
|
||||
? this.hass.localize(
|
||||
"ui.dialogs.more_info_control.exit_edit_mode"
|
||||
)
|
||||
: favoritesLabels?.editMode}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item
|
||||
value="reset_favorites"
|
||||
.disabled=${resetFavoritesDisabled}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiBackupRestore}
|
||||
></ha-svg-icon>
|
||||
${favoritesLabels?.reset}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="copy_favorites">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
${favoritesLabels?.copy}
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>
|
||||
`
|
||||
: nothing}
|
||||
${deviceId
|
||||
? html`
|
||||
<ha-dropdown-item value="device">
|
||||
@@ -698,50 +724,6 @@ export class MoreInfoDialog extends ScrollableFadeMixin(LitElement) {
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${this._entry &&
|
||||
stateObj &&
|
||||
domain === "light" &&
|
||||
lightSupportsFavoriteColors(stateObj)
|
||||
? html`
|
||||
<ha-dropdown-item value="toggle_edit">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${this._infoEditMode
|
||||
? mdiPencilOff
|
||||
: mdiPencil}
|
||||
></ha-svg-icon>
|
||||
${this._infoEditMode
|
||||
? this.hass.localize(
|
||||
`ui.dialogs.more_info_control.exit_edit_mode`
|
||||
)
|
||||
: this.hass.localize(
|
||||
`ui.dialogs.more_info_control.${domain}.edit_mode`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item
|
||||
value="reset_favorites"
|
||||
.disabled=${!this._entry.options?.light
|
||||
?.favorite_colors}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiBackupRestore}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.reset_favorites`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
<ha-dropdown-item value="copy_favorites">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiContentDuplicate}
|
||||
></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
`ui.dialogs.more_info_control.light.copy_favorites`
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
<ha-dropdown-item value="related">
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
|
||||
@@ -6,9 +6,8 @@ import { computeAttributeNameDisplay } from "../../../common/entity/compute_attr
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateActive } from "../../../common/entity/state_active";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-slider";
|
||||
import { CoverEntityFeature, type CoverEntity } from "../../../data/cover";
|
||||
import { coverSupportsPosition, type CoverEntity } from "../../../data/cover";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity/entity_attributes";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
@@ -28,10 +27,7 @@ export const supportsCoverPositionCardFeature = (
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "cover" &&
|
||||
supportsFeature(stateObj, CoverEntityFeature.SET_POSITION)
|
||||
);
|
||||
return domain === "cover" && coverSupportsPosition(stateObj);
|
||||
};
|
||||
|
||||
@customElement("hui-cover-position-card-feature")
|
||||
@@ -47,11 +43,11 @@ class HuiCoverPositionCardFeature
|
||||
|
||||
@state() private _config?: CoverPositionCardFeatureConfig;
|
||||
|
||||
private get _stateObj() {
|
||||
private get _stateObj(): CoverEntity | undefined {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
|
||||
return this.hass.states[this.context.entity_id!];
|
||||
}
|
||||
|
||||
static getStubConfig(): CoverPositionCardFeatureConfig {
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { CoverEntity } from "../../../data/cover";
|
||||
import {
|
||||
DEFAULT_COVER_FAVORITE_POSITIONS,
|
||||
coverSupportsPosition,
|
||||
normalizeCoverFavoritePositions,
|
||||
} from "../../../data/cover";
|
||||
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
getExtendedEntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
CoverPositionFavoriteCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "./types";
|
||||
|
||||
export const supportsCoverPositionFavoriteCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "cover" && coverSupportsPosition(stateObj);
|
||||
};
|
||||
|
||||
@customElement("hui-cover-position-favorite-card-feature")
|
||||
class HuiCoverPositionFavoriteCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@property({ attribute: false }) public color?: string;
|
||||
|
||||
@state() private _config?: CoverPositionFavoriteCardFeatureConfig;
|
||||
|
||||
@state() private _entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@state() private _currentPosition?: number;
|
||||
|
||||
private _unsubEntityRegistry?: UnsubscribeFunc;
|
||||
|
||||
private _subscribedEntityId?: string;
|
||||
|
||||
private _subscribedConnection?: HomeAssistant["connection"];
|
||||
|
||||
private get _stateObj(): CoverEntity | undefined {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!];
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._refreshEntitySubscription();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeEntityRegistry();
|
||||
}
|
||||
|
||||
static getStubConfig(): CoverPositionFavoriteCardFeatureConfig {
|
||||
return {
|
||||
type: "cover-position-favorite",
|
||||
};
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import("../editor/config-elements/hui-cover-position-favorite-card-feature-editor");
|
||||
return document.createElement(
|
||||
"hui-cover-position-favorite-card-feature-editor"
|
||||
);
|
||||
}
|
||||
|
||||
public setConfig(config: CoverPositionFavoriteCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
super.willUpdate(changedProp);
|
||||
if (
|
||||
(changedProp.has("hass") || changedProp.has("context")) &&
|
||||
this._stateObj
|
||||
) {
|
||||
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
|
||||
const oldStateObj = oldHass?.states[this.context!.entity_id!];
|
||||
if (oldStateObj !== this._stateObj) {
|
||||
this._currentPosition =
|
||||
this._stateObj.attributes.current_position ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
changedProp.has("context") &&
|
||||
(changedProp.get("context") as LovelaceCardFeatureContext | undefined)
|
||||
?.entity_id !== this.context?.entity_id
|
||||
) {
|
||||
this._refreshEntitySubscription();
|
||||
}
|
||||
|
||||
if (
|
||||
changedProp.has("hass") &&
|
||||
(changedProp.get("hass") as HomeAssistant | undefined)?.connection !==
|
||||
this.hass?.connection
|
||||
) {
|
||||
this._refreshEntitySubscription();
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshEntitySubscription(): void {
|
||||
this._ensureEntitySubscription().catch(() => undefined);
|
||||
}
|
||||
|
||||
private _unsubscribeEntityRegistry(): void {
|
||||
if (this._unsubEntityRegistry) {
|
||||
this._unsubEntityRegistry();
|
||||
this._unsubEntityRegistry = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadEntityEntry(entityId: string): Promise<void> {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = await getExtendedEntityRegistryEntry(this.hass, entityId);
|
||||
|
||||
if (this.context?.entity_id === entityId) {
|
||||
this._entry = entry;
|
||||
}
|
||||
} catch (_err) {
|
||||
if (this.context?.entity_id === entityId) {
|
||||
this._entry = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _subscribeEntityEntry(entityId: string): Promise<void> {
|
||||
this._unsubscribeEntityRegistry();
|
||||
|
||||
await this._loadEntityEntry(entityId);
|
||||
|
||||
try {
|
||||
this._unsubEntityRegistry = subscribeEntityRegistry(
|
||||
this.hass!.connection,
|
||||
async (entries) => {
|
||||
if (this.context?.entity_id !== entityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entries.some((entry) => entry.entity_id === entityId)) {
|
||||
await this._loadEntityEntry(entityId);
|
||||
return;
|
||||
}
|
||||
|
||||
this._entry = null;
|
||||
}
|
||||
);
|
||||
} catch (_err) {
|
||||
this._unsubEntityRegistry = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _ensureEntitySubscription(): Promise<void> {
|
||||
const entityId = this.context?.entity_id;
|
||||
const connection = this.hass?.connection;
|
||||
|
||||
if (!this.hass || !entityId || !connection) {
|
||||
this._unsubscribeEntityRegistry();
|
||||
this._subscribedEntityId = undefined;
|
||||
this._subscribedConnection = undefined;
|
||||
this._entry = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this._subscribedEntityId === entityId &&
|
||||
this._subscribedConnection === connection &&
|
||||
this._unsubEntityRegistry
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._subscribedEntityId = entityId;
|
||||
this._subscribedConnection = connection;
|
||||
|
||||
await this._subscribeEntityEntry(entityId);
|
||||
}
|
||||
|
||||
private async _valueChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["value-changed"]>
|
||||
) {
|
||||
const value = ev.detail.value;
|
||||
if (value == null) return;
|
||||
|
||||
const position = Number(value);
|
||||
if (isNaN(position)) return;
|
||||
|
||||
const oldPosition = this._stateObj!.attributes.current_position;
|
||||
if (position === oldPosition) return;
|
||||
|
||||
this._currentPosition = position;
|
||||
try {
|
||||
await this.hass!.callService("cover", "set_cover_position", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
position: position,
|
||||
});
|
||||
} catch (_err) {
|
||||
this._currentPosition = oldPosition ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateObj ||
|
||||
!supportsCoverPositionFavoriteCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const positions = normalizeCoverFavoritePositions(
|
||||
this._entry?.options?.cover?.favorite_positions ??
|
||||
DEFAULT_COVER_FAVORITE_POSITIONS
|
||||
);
|
||||
|
||||
if (positions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = positions.map((position) => ({
|
||||
value: String(position),
|
||||
label: `${position}%`,
|
||||
ariaLabel: this.hass!.localize(
|
||||
"ui.dialogs.more_info_control.cover.favorite_position.set",
|
||||
{ value: `${position}%` }
|
||||
),
|
||||
}));
|
||||
|
||||
const currentValue =
|
||||
this._currentPosition != null ? String(this._currentPosition) : undefined;
|
||||
|
||||
const color = this.color
|
||||
? computeCssColor(this.color)
|
||||
: stateColorCss(this._stateObj);
|
||||
|
||||
const style = {
|
||||
"--feature-color": color,
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-control-select
|
||||
style=${styleMap(style)}
|
||||
.options=${options}
|
||||
.value=${currentValue}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-position-favorite.label"
|
||||
)}
|
||||
.disabled=${this._stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-select>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return cardFeatureStyles;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-cover-position-favorite-card-feature": HuiCoverPositionFavoriteCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { CoverEntity } from "../../../data/cover";
|
||||
import { CoverEntityFeature } from "../../../data/cover";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
CoverPositionPresetCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "./types";
|
||||
|
||||
export const DEFAULT_COVER_POSITION_PRESETS = [0, 25, 75, 100];
|
||||
|
||||
export const supportsCoverPositionPresetCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "cover" &&
|
||||
supportsFeature(stateObj, CoverEntityFeature.SET_POSITION)
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-cover-position-preset-card-feature")
|
||||
class HuiCoverPositionPresetCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@property({ attribute: false }) public color?: string;
|
||||
|
||||
@state() private _config?: CoverPositionPresetCardFeatureConfig;
|
||||
|
||||
@state() private _currentPosition?: number;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(): CoverPositionPresetCardFeatureConfig {
|
||||
return {
|
||||
type: "cover-position-preset",
|
||||
positions: DEFAULT_COVER_POSITION_PRESETS,
|
||||
};
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import("../editor/config-elements/hui-cover-position-preset-card-feature-editor");
|
||||
return document.createElement(
|
||||
"hui-cover-position-preset-card-feature-editor"
|
||||
);
|
||||
}
|
||||
|
||||
public setConfig(config: CoverPositionPresetCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
super.willUpdate(changedProp);
|
||||
if (
|
||||
(changedProp.has("hass") || changedProp.has("context")) &&
|
||||
this._stateObj
|
||||
) {
|
||||
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
|
||||
const oldStateObj = oldHass?.states[this.context!.entity_id!];
|
||||
if (oldStateObj !== this._stateObj) {
|
||||
this._currentPosition =
|
||||
this._stateObj.attributes.current_position ?? undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _valueChanged(
|
||||
ev: CustomEvent<{ value?: string; item?: { value: string } }>
|
||||
) {
|
||||
const value = ev.detail.value ?? ev.detail.item?.value;
|
||||
if (value == null) return;
|
||||
|
||||
const position = Number(value);
|
||||
if (isNaN(position)) return;
|
||||
|
||||
const oldPosition = this._stateObj!.attributes.current_position;
|
||||
if (position === oldPosition) return;
|
||||
|
||||
this._currentPosition = position;
|
||||
try {
|
||||
await this.hass!.callService("cover", "set_cover_position", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
position: position,
|
||||
});
|
||||
} catch (_err) {
|
||||
this._currentPosition = oldPosition ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateObj ||
|
||||
!supportsCoverPositionPresetCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const positions = this._config.positions ?? DEFAULT_COVER_POSITION_PRESETS;
|
||||
|
||||
const options = positions.map((position) => ({
|
||||
value: String(position),
|
||||
label: `${position}%`,
|
||||
}));
|
||||
|
||||
const currentValue =
|
||||
this._currentPosition != null ? String(this._currentPosition) : undefined;
|
||||
|
||||
const color = this.color
|
||||
? computeCssColor(this.color)
|
||||
: stateColorCss(this._stateObj);
|
||||
|
||||
const style = {
|
||||
"--feature-color": color,
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-control-select
|
||||
style=${styleMap(style)}
|
||||
.options=${options}
|
||||
.value=${currentValue}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-position-preset.label"
|
||||
)}
|
||||
.disabled=${this._stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-select>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return cardFeatureStyles;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-cover-position-preset-card-feature": HuiCoverPositionPresetCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { CoverEntity } from "../../../data/cover";
|
||||
import {
|
||||
DEFAULT_COVER_FAVORITE_POSITIONS,
|
||||
coverSupportsTiltPosition,
|
||||
normalizeCoverFavoritePositions,
|
||||
} from "../../../data/cover";
|
||||
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
getExtendedEntityRegistryEntry,
|
||||
subscribeEntityRegistry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
CoverTiltFavoriteCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "./types";
|
||||
|
||||
export const supportsCoverTiltFavoriteCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return domain === "cover" && coverSupportsTiltPosition(stateObj);
|
||||
};
|
||||
|
||||
@customElement("hui-cover-tilt-favorite-card-feature")
|
||||
class HuiCoverTiltFavoriteCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@property({ attribute: false }) public color?: string;
|
||||
|
||||
@state() private _config?: CoverTiltFavoriteCardFeatureConfig;
|
||||
|
||||
@state() private _entry?: ExtEntityRegistryEntry | null;
|
||||
|
||||
@state() private _currentTiltPosition?: number;
|
||||
|
||||
private _unsubEntityRegistry?: UnsubscribeFunc;
|
||||
|
||||
private _subscribedEntityId?: string;
|
||||
|
||||
private _subscribedConnection?: HomeAssistant["connection"];
|
||||
|
||||
private get _stateObj(): CoverEntity | undefined {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!];
|
||||
}
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._refreshEntitySubscription();
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeEntityRegistry();
|
||||
}
|
||||
|
||||
static getStubConfig(): CoverTiltFavoriteCardFeatureConfig {
|
||||
return {
|
||||
type: "cover-tilt-favorite",
|
||||
};
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import("../editor/config-elements/hui-cover-tilt-favorite-card-feature-editor");
|
||||
return document.createElement(
|
||||
"hui-cover-tilt-favorite-card-feature-editor"
|
||||
);
|
||||
}
|
||||
|
||||
public setConfig(config: CoverTiltFavoriteCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
super.willUpdate(changedProp);
|
||||
if (
|
||||
(changedProp.has("hass") || changedProp.has("context")) &&
|
||||
this._stateObj
|
||||
) {
|
||||
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
|
||||
const oldStateObj = oldHass?.states[this.context!.entity_id!];
|
||||
if (oldStateObj !== this._stateObj) {
|
||||
this._currentTiltPosition =
|
||||
this._stateObj.attributes.current_tilt_position ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
changedProp.has("context") &&
|
||||
(changedProp.get("context") as LovelaceCardFeatureContext | undefined)
|
||||
?.entity_id !== this.context?.entity_id
|
||||
) {
|
||||
this._refreshEntitySubscription();
|
||||
}
|
||||
|
||||
if (
|
||||
changedProp.has("hass") &&
|
||||
(changedProp.get("hass") as HomeAssistant | undefined)?.connection !==
|
||||
this.hass?.connection
|
||||
) {
|
||||
this._refreshEntitySubscription();
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshEntitySubscription(): void {
|
||||
this._ensureEntitySubscription().catch(() => undefined);
|
||||
}
|
||||
|
||||
private _unsubscribeEntityRegistry(): void {
|
||||
if (this._unsubEntityRegistry) {
|
||||
this._unsubEntityRegistry();
|
||||
this._unsubEntityRegistry = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadEntityEntry(entityId: string): Promise<void> {
|
||||
if (!this.hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const entry = await getExtendedEntityRegistryEntry(this.hass, entityId);
|
||||
|
||||
if (this.context?.entity_id === entityId) {
|
||||
this._entry = entry;
|
||||
}
|
||||
} catch (_err) {
|
||||
if (this.context?.entity_id === entityId) {
|
||||
this._entry = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _subscribeEntityEntry(entityId: string): Promise<void> {
|
||||
this._unsubscribeEntityRegistry();
|
||||
|
||||
await this._loadEntityEntry(entityId);
|
||||
|
||||
try {
|
||||
this._unsubEntityRegistry = subscribeEntityRegistry(
|
||||
this.hass!.connection,
|
||||
async (entries) => {
|
||||
if (this.context?.entity_id !== entityId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (entries.some((entry) => entry.entity_id === entityId)) {
|
||||
await this._loadEntityEntry(entityId);
|
||||
return;
|
||||
}
|
||||
|
||||
this._entry = null;
|
||||
}
|
||||
);
|
||||
} catch (_err) {
|
||||
this._unsubEntityRegistry = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _ensureEntitySubscription(): Promise<void> {
|
||||
const entityId = this.context?.entity_id;
|
||||
const connection = this.hass?.connection;
|
||||
|
||||
if (!this.hass || !entityId || !connection) {
|
||||
this._unsubscribeEntityRegistry();
|
||||
this._subscribedEntityId = undefined;
|
||||
this._subscribedConnection = undefined;
|
||||
this._entry = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
this._subscribedEntityId === entityId &&
|
||||
this._subscribedConnection === connection &&
|
||||
this._unsubEntityRegistry
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._subscribedEntityId = entityId;
|
||||
this._subscribedConnection = connection;
|
||||
|
||||
await this._subscribeEntityEntry(entityId);
|
||||
}
|
||||
|
||||
private async _valueChanged(
|
||||
ev: HASSDomEvent<HASSDomEvents["value-changed"]>
|
||||
) {
|
||||
if (ev.detail.value == null) return;
|
||||
|
||||
const tiltPosition = Number(ev.detail.value);
|
||||
if (isNaN(tiltPosition)) return;
|
||||
|
||||
const oldTiltPosition = this._stateObj!.attributes.current_tilt_position;
|
||||
if (tiltPosition === oldTiltPosition) return;
|
||||
|
||||
this._currentTiltPosition = tiltPosition;
|
||||
try {
|
||||
await this.hass!.callService("cover", "set_cover_tilt_position", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
tilt_position: tiltPosition,
|
||||
});
|
||||
} catch (_err) {
|
||||
this._currentTiltPosition = oldTiltPosition ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateObj ||
|
||||
!supportsCoverTiltFavoriteCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const positions = normalizeCoverFavoritePositions(
|
||||
this._entry?.options?.cover?.favorite_tilt_positions ??
|
||||
DEFAULT_COVER_FAVORITE_POSITIONS
|
||||
);
|
||||
|
||||
if (positions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = positions.map((position) => ({
|
||||
value: String(position),
|
||||
label: `${position}%`,
|
||||
ariaLabel: this.hass!.localize(
|
||||
"ui.dialogs.more_info_control.cover.favorite_tilt_position.set",
|
||||
{ value: `${position}%` }
|
||||
),
|
||||
}));
|
||||
|
||||
const currentValue =
|
||||
this._currentTiltPosition != null
|
||||
? String(this._currentTiltPosition)
|
||||
: undefined;
|
||||
|
||||
const color = this.color
|
||||
? computeCssColor(this.color)
|
||||
: stateColorCss(this._stateObj);
|
||||
|
||||
const style = {
|
||||
"--feature-color": color,
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-control-select
|
||||
style=${styleMap(style)}
|
||||
.options=${options}
|
||||
.value=${currentValue}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-tilt-favorite.label"
|
||||
)}
|
||||
.disabled=${this._stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-select>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return cardFeatureStyles;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-cover-tilt-favorite-card-feature": HuiCoverTiltFavoriteCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,8 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { computeAttributeNameDisplay } from "../../../common/entity/compute_attribute_display";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import type { CoverEntity } from "../../../data/cover";
|
||||
import { CoverEntityFeature } from "../../../data/cover";
|
||||
import { coverSupportsTiltPosition } from "../../../data/cover";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../data/entity/entity_attributes";
|
||||
import { generateTiltSliderTrackBackgroundGradient } from "../../../state-control/cover/ha-state-control-cover-tilt-position";
|
||||
@@ -31,8 +30,7 @@ export const supportsCoverTiltPositionCardFeature = (
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "cover" &&
|
||||
supportsFeature(stateObj, CoverEntityFeature.SET_TILT_POSITION)
|
||||
domain === "cover" && coverSupportsTiltPosition(stateObj as CoverEntity)
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||
import { stateColorCss } from "../../../common/entity/state_color";
|
||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||
import "../../../components/ha-control-select";
|
||||
import type { CoverEntity } from "../../../data/cover";
|
||||
import { CoverEntityFeature } from "../../../data/cover";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types";
|
||||
import { cardFeatureStyles } from "./common/card-feature-styles";
|
||||
import type {
|
||||
CoverTiltPresetCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "./types";
|
||||
|
||||
export const DEFAULT_COVER_TILT_PRESETS = [0, 25, 75, 100];
|
||||
|
||||
export const supportsCoverTiltPresetCardFeature = (
|
||||
hass: HomeAssistant,
|
||||
context: LovelaceCardFeatureContext
|
||||
) => {
|
||||
const stateObj = context.entity_id
|
||||
? hass.states[context.entity_id]
|
||||
: undefined;
|
||||
if (!stateObj) return false;
|
||||
const domain = computeDomain(stateObj.entity_id);
|
||||
return (
|
||||
domain === "cover" &&
|
||||
supportsFeature(stateObj, CoverEntityFeature.SET_TILT_POSITION)
|
||||
);
|
||||
};
|
||||
|
||||
@customElement("hui-cover-tilt-preset-card-feature")
|
||||
class HuiCoverTiltPresetCardFeature
|
||||
extends LitElement
|
||||
implements LovelaceCardFeature
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@property({ attribute: false }) public color?: string;
|
||||
|
||||
@state() private _config?: CoverTiltPresetCardFeatureConfig;
|
||||
|
||||
@state() private _currentTiltPosition?: number;
|
||||
|
||||
private get _stateObj() {
|
||||
if (!this.hass || !this.context || !this.context.entity_id) {
|
||||
return undefined;
|
||||
}
|
||||
return this.hass.states[this.context.entity_id!] as CoverEntity | undefined;
|
||||
}
|
||||
|
||||
static getStubConfig(): CoverTiltPresetCardFeatureConfig {
|
||||
return {
|
||||
type: "cover-tilt-preset",
|
||||
positions: DEFAULT_COVER_TILT_PRESETS,
|
||||
};
|
||||
}
|
||||
|
||||
public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> {
|
||||
await import("../editor/config-elements/hui-cover-tilt-preset-card-feature-editor");
|
||||
return document.createElement("hui-cover-tilt-preset-card-feature-editor");
|
||||
}
|
||||
|
||||
public setConfig(config: CoverTiltPresetCardFeatureConfig): void {
|
||||
if (!config) {
|
||||
throw new Error("Invalid configuration");
|
||||
}
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProp: PropertyValues): void {
|
||||
super.willUpdate(changedProp);
|
||||
if (
|
||||
(changedProp.has("hass") || changedProp.has("context")) &&
|
||||
this._stateObj
|
||||
) {
|
||||
const oldHass = changedProp.get("hass") as HomeAssistant | undefined;
|
||||
const oldStateObj = oldHass?.states[this.context!.entity_id!];
|
||||
if (oldStateObj !== this._stateObj) {
|
||||
this._currentTiltPosition =
|
||||
this._stateObj.attributes.current_tilt_position ?? undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _valueChanged(
|
||||
ev: CustomEvent<{ value?: string; item?: { value: string } }>
|
||||
) {
|
||||
const value = ev.detail.value ?? ev.detail.item?.value;
|
||||
if (value == null) return;
|
||||
|
||||
const tiltPosition = Number(value);
|
||||
if (isNaN(tiltPosition)) return;
|
||||
|
||||
const oldTiltPosition = this._stateObj!.attributes.current_tilt_position;
|
||||
if (tiltPosition === oldTiltPosition) return;
|
||||
|
||||
this._currentTiltPosition = tiltPosition;
|
||||
try {
|
||||
await this.hass!.callService("cover", "set_cover_tilt_position", {
|
||||
entity_id: this._stateObj!.entity_id,
|
||||
tilt_position: tiltPosition,
|
||||
});
|
||||
} catch (_err) {
|
||||
this._currentTiltPosition = oldTiltPosition ?? undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | null {
|
||||
if (
|
||||
!this._config ||
|
||||
!this.hass ||
|
||||
!this.context ||
|
||||
!this._stateObj ||
|
||||
!supportsCoverTiltPresetCardFeature(this.hass, this.context)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const positions = this._config.positions ?? DEFAULT_COVER_TILT_PRESETS;
|
||||
|
||||
const options = positions.map((position) => ({
|
||||
value: String(position),
|
||||
label: `${position}%`,
|
||||
}));
|
||||
|
||||
const currentValue =
|
||||
this._currentTiltPosition != null
|
||||
? String(this._currentTiltPosition)
|
||||
: undefined;
|
||||
|
||||
const color = this.color
|
||||
? computeCssColor(this.color)
|
||||
: stateColorCss(this._stateObj);
|
||||
|
||||
const style = {
|
||||
"--feature-color": color,
|
||||
};
|
||||
|
||||
return html`
|
||||
<ha-control-select
|
||||
style=${styleMap(style)}
|
||||
.options=${options}
|
||||
.value=${currentValue}
|
||||
@value-changed=${this._valueChanged}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-tilt-preset.label"
|
||||
)}
|
||||
.disabled=${this._stateObj!.state === UNAVAILABLE}
|
||||
>
|
||||
</ha-control-select>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
return cardFeatureStyles;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-cover-tilt-preset-card-feature": HuiCoverTiltPresetCardFeature;
|
||||
}
|
||||
}
|
||||
@@ -26,14 +26,12 @@ export interface CoverTiltPositionCardFeatureConfig {
|
||||
type: "cover-tilt-position";
|
||||
}
|
||||
|
||||
export interface CoverPositionPresetCardFeatureConfig {
|
||||
type: "cover-position-preset";
|
||||
positions?: number[];
|
||||
export interface CoverPositionFavoriteCardFeatureConfig {
|
||||
type: "cover-position-favorite";
|
||||
}
|
||||
|
||||
export interface CoverTiltPresetCardFeatureConfig {
|
||||
type: "cover-tilt-preset";
|
||||
positions?: number[];
|
||||
export interface CoverTiltFavoriteCardFeatureConfig {
|
||||
type: "cover-tilt-favorite";
|
||||
}
|
||||
|
||||
export interface LightBrightnessCardFeatureConfig {
|
||||
@@ -260,8 +258,8 @@ export type LovelaceCardFeatureConfig =
|
||||
| CounterActionsCardFeatureConfig
|
||||
| CoverOpenCloseCardFeatureConfig
|
||||
| CoverPositionCardFeatureConfig
|
||||
| CoverPositionPresetCardFeatureConfig
|
||||
| CoverTiltPresetCardFeatureConfig
|
||||
| CoverPositionFavoriteCardFeatureConfig
|
||||
| CoverTiltFavoriteCardFeatureConfig
|
||||
| CoverTiltPositionCardFeatureConfig
|
||||
| CoverTiltCardFeatureConfig
|
||||
| DateSetCardFeatureConfig
|
||||
|
||||
@@ -7,10 +7,10 @@ import "../card-features/hui-climate-swing-horizontal-modes-card-feature";
|
||||
import "../card-features/hui-climate-swing-modes-card-feature";
|
||||
import "../card-features/hui-counter-actions-card-feature";
|
||||
import "../card-features/hui-cover-open-close-card-feature";
|
||||
import "../card-features/hui-cover-position-favorite-card-feature";
|
||||
import "../card-features/hui-cover-position-card-feature";
|
||||
import "../card-features/hui-cover-position-preset-card-feature";
|
||||
import "../card-features/hui-cover-tilt-card-feature";
|
||||
import "../card-features/hui-cover-tilt-preset-card-feature";
|
||||
import "../card-features/hui-cover-tilt-favorite-card-feature";
|
||||
import "../card-features/hui-cover-tilt-position-card-feature";
|
||||
import "../card-features/hui-date-set-card-feature";
|
||||
import "../card-features/hui-fan-direction-card-feature";
|
||||
@@ -60,9 +60,9 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([
|
||||
"climate-preset-modes",
|
||||
"counter-actions",
|
||||
"cover-open-close",
|
||||
"cover-position-favorite",
|
||||
"cover-position",
|
||||
"cover-position-preset",
|
||||
"cover-tilt-preset",
|
||||
"cover-tilt-favorite",
|
||||
"cover-tilt-position",
|
||||
"cover-tilt",
|
||||
"date-set",
|
||||
|
||||
@@ -35,10 +35,10 @@ import { supportsClimateSwingHorizontalModesCardFeature } from "../../card-featu
|
||||
import { supportsClimateSwingModesCardFeature } from "../../card-features/hui-climate-swing-modes-card-feature";
|
||||
import { supportsCounterActionsCardFeature } from "../../card-features/hui-counter-actions-card-feature";
|
||||
import { supportsCoverOpenCloseCardFeature } from "../../card-features/hui-cover-open-close-card-feature";
|
||||
import { supportsCoverPositionFavoriteCardFeature } from "../../card-features/hui-cover-position-favorite-card-feature";
|
||||
import { supportsCoverPositionCardFeature } from "../../card-features/hui-cover-position-card-feature";
|
||||
import { supportsCoverPositionPresetCardFeature } from "../../card-features/hui-cover-position-preset-card-feature";
|
||||
import { supportsCoverTiltCardFeature } from "../../card-features/hui-cover-tilt-card-feature";
|
||||
import { supportsCoverTiltPresetCardFeature } from "../../card-features/hui-cover-tilt-preset-card-feature";
|
||||
import { supportsCoverTiltFavoriteCardFeature } from "../../card-features/hui-cover-tilt-favorite-card-feature";
|
||||
import { supportsCoverTiltPositionCardFeature } from "../../card-features/hui-cover-tilt-position-card-feature";
|
||||
import { supportsDateSetCardFeature } from "../../card-features/hui-date-set-card-feature";
|
||||
import { supportsFanDirectionCardFeature } from "../../card-features/hui-fan-direction-card-feature";
|
||||
@@ -92,9 +92,9 @@ const UI_FEATURE_TYPES = [
|
||||
"climate-swing-horizontal-modes",
|
||||
"counter-actions",
|
||||
"cover-open-close",
|
||||
"cover-position-favorite",
|
||||
"cover-position",
|
||||
"cover-position-preset",
|
||||
"cover-tilt-preset",
|
||||
"cover-tilt-favorite",
|
||||
"cover-tilt-position",
|
||||
"cover-tilt",
|
||||
"date-set",
|
||||
@@ -139,8 +139,8 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([
|
||||
"climate-swing-modes",
|
||||
"climate-swing-horizontal-modes",
|
||||
"counter-actions",
|
||||
"cover-position-preset",
|
||||
"cover-tilt-preset",
|
||||
"cover-position-favorite",
|
||||
"cover-tilt-favorite",
|
||||
"fan-preset-modes",
|
||||
"humidifier-modes",
|
||||
"lawn-mower-commands",
|
||||
@@ -169,9 +169,9 @@ const SUPPORTS_FEATURE_TYPES: Record<
|
||||
"climate-preset-modes": supportsClimatePresetModesCardFeature,
|
||||
"counter-actions": supportsCounterActionsCardFeature,
|
||||
"cover-open-close": supportsCoverOpenCloseCardFeature,
|
||||
"cover-position-favorite": supportsCoverPositionFavoriteCardFeature,
|
||||
"cover-position": supportsCoverPositionCardFeature,
|
||||
"cover-position-preset": supportsCoverPositionPresetCardFeature,
|
||||
"cover-tilt-preset": supportsCoverTiltPresetCardFeature,
|
||||
"cover-tilt-favorite": supportsCoverTiltFavoriteCardFeature,
|
||||
"cover-tilt-position": supportsCoverTiltPositionCardFeature,
|
||||
"cover-tilt": supportsCoverTiltCardFeature,
|
||||
"date-set": supportsDateSetCardFeature,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../components/ha-alert";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
CoverPositionFavoriteCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
@customElement("hui-cover-position-favorite-card-feature-editor")
|
||||
export class HuiCoverPositionFavoriteCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: CoverPositionFavoriteCardFeatureConfig;
|
||||
|
||||
public setConfig(config: CoverPositionFavoriteCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-position-favorite.description"
|
||||
)}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-cover-position-favorite-card-feature-editor": HuiCoverPositionFavoriteCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-multi-textfield";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { DEFAULT_COVER_POSITION_PRESETS } from "../../card-features/hui-cover-position-preset-card-feature";
|
||||
import type {
|
||||
CoverPositionPresetCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
@customElement("hui-cover-position-preset-card-feature-editor")
|
||||
export class HuiCoverPositionPresetCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: CoverPositionPresetCardFeatureConfig;
|
||||
|
||||
public setConfig(config: CoverPositionPresetCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const positions = this._config.positions ?? DEFAULT_COVER_POSITION_PRESETS;
|
||||
|
||||
const stringValues = positions.map((p) => String(p));
|
||||
|
||||
return html`
|
||||
<ha-multi-textfield
|
||||
.hass=${this.hass}
|
||||
.value=${stringValues}
|
||||
.max=${6}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-position-preset.position"
|
||||
)}
|
||||
.inputType=${"number"}
|
||||
.inputSuffix=${"%"}
|
||||
.addLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-position-preset.add_position"
|
||||
)}
|
||||
.removeLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-position-preset.remove_position"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-multi-textfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const stringValues = ev.detail.value as (string | null | undefined)[];
|
||||
const positions = stringValues
|
||||
.filter((v): v is string => !!v && !isNaN(Number(v)))
|
||||
.map((v) => Math.min(100, Math.max(0, Number(v))));
|
||||
|
||||
const config: CoverPositionPresetCardFeatureConfig = {
|
||||
...this._config!,
|
||||
positions,
|
||||
};
|
||||
|
||||
this._config = config;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-cover-position-preset-card-feature-editor": HuiCoverPositionPresetCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../components/ha-alert";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type {
|
||||
CoverTiltFavoriteCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
@customElement("hui-cover-tilt-favorite-card-feature-editor")
|
||||
export class HuiCoverTiltFavoriteCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: CoverTiltFavoriteCardFeatureConfig;
|
||||
|
||||
public setConfig(config: CoverTiltFavoriteCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-tilt-favorite.description"
|
||||
)}
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-cover-tilt-favorite-card-feature-editor": HuiCoverTiltFavoriteCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-multi-textfield";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { DEFAULT_COVER_TILT_PRESETS } from "../../card-features/hui-cover-tilt-preset-card-feature";
|
||||
import type {
|
||||
CoverTiltPresetCardFeatureConfig,
|
||||
LovelaceCardFeatureContext,
|
||||
} from "../../card-features/types";
|
||||
import type { LovelaceCardFeatureEditor } from "../../types";
|
||||
|
||||
@customElement("hui-cover-tilt-preset-card-feature-editor")
|
||||
export class HuiCoverTiltPresetCardFeatureEditor
|
||||
extends LitElement
|
||||
implements LovelaceCardFeatureEditor
|
||||
{
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
|
||||
|
||||
@state() private _config?: CoverTiltPresetCardFeatureConfig;
|
||||
|
||||
public setConfig(config: CoverTiltPresetCardFeatureConfig): void {
|
||||
this._config = config;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.hass || !this._config) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const positions = this._config.positions ?? DEFAULT_COVER_TILT_PRESETS;
|
||||
|
||||
const stringValues = positions.map((p) => String(p));
|
||||
|
||||
return html`
|
||||
<ha-multi-textfield
|
||||
.hass=${this.hass}
|
||||
.value=${stringValues}
|
||||
.max=${6}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-tilt-preset.position"
|
||||
)}
|
||||
.inputType=${"number"}
|
||||
.inputSuffix=${"%"}
|
||||
.addLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-tilt-preset.add_position"
|
||||
)}
|
||||
.removeLabel=${this.hass.localize(
|
||||
"ui.panel.lovelace.editor.features.types.cover-tilt-preset.remove_position"
|
||||
)}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-multi-textfield>
|
||||
`;
|
||||
}
|
||||
|
||||
private _valueChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
const stringValues = ev.detail.value as (string | null | undefined)[];
|
||||
const positions = stringValues
|
||||
.filter((v): v is string => !!v && !isNaN(Number(v)))
|
||||
.map((v) => Math.min(100, Math.max(0, Number(v))));
|
||||
|
||||
const config: CoverTiltPresetCardFeatureConfig = {
|
||||
...this._config!,
|
||||
positions,
|
||||
};
|
||||
|
||||
this._config = config;
|
||||
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"hui-cover-tilt-preset-card-feature-editor": HuiCoverTiltPresetCardFeatureEditor;
|
||||
}
|
||||
}
|
||||
@@ -1603,9 +1603,37 @@
|
||||
"create_zone": "Create zone from current location"
|
||||
},
|
||||
"cover": {
|
||||
"edit_mode": "Edit favorites",
|
||||
"reset_favorites": "Reset favorites",
|
||||
"reset_favorites_text": "Do you want to reset all favorites for this cover back to default?",
|
||||
"copy_favorites": "Copy favorites to...",
|
||||
"copy_favorites_helper": "Select one or more covers to receive the copied favorites from this cover. Covers must support the same position controls.",
|
||||
"copy_favorites_entities": "[%key:ui::panel::lovelace::editor::card::generic::entities%]",
|
||||
"switch_mode": {
|
||||
"button": "Switch to button mode",
|
||||
"position": "Switch to position mode"
|
||||
},
|
||||
"favorite_position": {
|
||||
"set": "Set position to {value}",
|
||||
"edit": "Set position to {value}",
|
||||
"delete": "Delete favorite position {number}",
|
||||
"delete_confirm_title": "Delete favorite position?",
|
||||
"delete_confirm_text": "This favorite position will be permanently deleted.",
|
||||
"delete_confirm_action": "Delete",
|
||||
"add": "Add new favorite position",
|
||||
"edit_title": "Edit favorite position",
|
||||
"add_title": "Add favorite position"
|
||||
},
|
||||
"favorite_tilt_position": {
|
||||
"set": "Set tilt position to {value}",
|
||||
"edit": "Set tilt position to {value}",
|
||||
"delete": "Delete favorite tilt position {number}",
|
||||
"delete_confirm_title": "Delete favorite tilt position?",
|
||||
"delete_confirm_text": "This favorite tilt position will be permanently deleted.",
|
||||
"delete_confirm_action": "Delete",
|
||||
"add": "Add new favorite tilt position",
|
||||
"edit_title": "Edit favorite tilt position",
|
||||
"add_title": "Add favorite tilt position"
|
||||
}
|
||||
},
|
||||
"zone": {
|
||||
@@ -9512,20 +9540,16 @@
|
||||
"cover-position": {
|
||||
"label": "Cover position"
|
||||
},
|
||||
"cover-position-preset": {
|
||||
"label": "Cover position preset",
|
||||
"position": "Position",
|
||||
"add_position": "Add position",
|
||||
"remove_position": "Remove position"
|
||||
"cover-position-favorite": {
|
||||
"label": "Cover favorite positions",
|
||||
"description": "This feature uses the cover's favorite positions. To edit them, open the cover's more info dialog and press and hold a favorite."
|
||||
},
|
||||
"cover-tilt": {
|
||||
"label": "Cover tilt"
|
||||
},
|
||||
"cover-tilt-preset": {
|
||||
"label": "Cover tilt preset",
|
||||
"position": "Position",
|
||||
"add_position": "Add position",
|
||||
"remove_position": "Remove position"
|
||||
"cover-tilt-favorite": {
|
||||
"label": "Cover favorite tilt positions",
|
||||
"description": "This feature uses the cover's favorite tilt positions. To edit them, open the cover's more info dialog and press and hold a favorite."
|
||||
},
|
||||
"cover-tilt-position": {
|
||||
"label": "Cover tilt position"
|
||||
|
||||
Reference in New Issue
Block a user