Compare commits

...

4 Commits

Author SHA1 Message Date
Aidan Timson
ca9561bdf1 Deduplicate favorite card feature editors 2026-03-18 12:14:20 +00:00
Aidan Timson
8504faae93 Extract shared favorites registry entry controller 2026-03-18 12:14:20 +00:00
Aidan Timson
481d7181b9 Extract shared light favorites helpers 2026-03-18 12:14:20 +00:00
Aidan Timson
0c92f60c2a Implement shared numeric favorites controller for more info flows 2026-03-18 12:14:20 +00:00
13 changed files with 701 additions and 896 deletions

View File

@@ -3,6 +3,7 @@ import type {
HassEntityBase,
} from "home-assistant-js-websocket";
import { temperature2rgb } from "../common/color/convert-light-color";
import type { HomeAssistant } from "../types";
export const enum LightEntityFeature {
EFFECT = 4,
@@ -160,4 +161,19 @@ export const computeDefaultFavoriteColors = (
return colors;
};
export const resolveLightFavoriteColors = (
stateObj: LightEntity,
favoriteColors?: LightColor[] | null
): LightColor[] => favoriteColors ?? computeDefaultFavoriteColors(stateObj);
export const applyLightFavoriteColor = (
hass: HomeAssistant,
stateObj: LightEntity,
favorite: LightColor
) =>
hass.callService("light", "turn_on", {
entity_id: stateObj.entity_id,
...favorite,
});
export const formatTempColor = (value: number) => `${value} K`;

View File

@@ -1,11 +1,6 @@
import type { PropertyValues, TemplateResult } from "lit";
import type { 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 { customElement, property } from "lit/decorators";
import type { CoverEntity } from "../../../../data/cover";
import {
DEFAULT_COVER_FAVORITE_POSITIONS,
@@ -15,35 +10,16 @@ import {
} 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 { ExtEntityRegistryEntry } 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";
import {
NumericMoreInfoFavoritesController,
type NumericFavoriteLocalizeKey,
} from "../numeric-more-info-favorites-controller";
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;
@@ -54,35 +30,57 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
@property({ attribute: false }) public editMode?: boolean;
@state() private _favoritePositions: number[] = [];
private readonly _positionFavorites =
new NumericMoreInfoFavoritesController<CoverEntity>(this, {
getHass: () => this.hass,
getStateObj: () => this.stateObj,
getEntry: () => this.entry,
getEditMode: () => this.editMode ?? false,
domain: "cover",
option: "favorite_positions",
defaultFavorites: DEFAULT_COVER_FAVORITE_POSITIONS,
getStoredFavorites: (entry) => entry.options?.cover?.favorite_positions,
normalizeFavorites: normalizeCoverFavoritePositions,
getCurrentValue: (stateObj) => {
const current = stateObj.attributes.current_position;
@state() private _favoriteTiltPositions: number[] = [];
return current == null ? undefined : Math.round(current);
},
setPositionService: "set_cover_position",
serviceDataKey: "position",
localize: (key, values) =>
this._localizeFavorite("position", key, values),
getInputLabel: () => this.hass.localize("ui.card.cover.position"),
inputSuffix: DOMAIN_ATTRIBUTES_UNITS.cover.current_position,
});
protected updated(changedProps: PropertyValues<this>): void {
if (
(changedProps.has("entry") || changedProps.has("stateObj")) &&
this.entry &&
this.stateObj
) {
const options = this.entry.options?.cover;
private readonly _tiltFavorites =
new NumericMoreInfoFavoritesController<CoverEntity>(this, {
getHass: () => this.hass,
getStateObj: () => this.stateObj,
getEntry: () => this.entry,
getEditMode: () => this.editMode ?? false,
domain: "cover",
option: "favorite_tilt_positions",
defaultFavorites: DEFAULT_COVER_FAVORITE_POSITIONS,
getStoredFavorites: (entry) =>
entry.options?.cover?.favorite_tilt_positions,
normalizeFavorites: normalizeCoverFavoritePositions,
getCurrentValue: (stateObj) => {
const current = stateObj.attributes.current_tilt_position;
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
)
: [];
}
}
return current == null ? undefined : Math.round(current);
},
setPositionService: "set_cover_tilt_position",
serviceDataKey: "tilt_position",
localize: (key, values) => this._localizeFavorite("tilt", key, values),
getInputLabel: () => this.hass.localize("ui.card.cover.tilt_position"),
inputSuffix: DOMAIN_ATTRIBUTES_UNITS.cover.current_position,
});
private _localizeFavorite(
kind: FavoriteKind,
key: FavoriteLocalizeKey,
key: NumericFavoriteLocalizeKey,
values?: Record<string, string | number>
): string {
return this.hass.localize(
@@ -91,277 +89,15 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
);
}
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[],
controller: NumericMoreInfoFavoritesController<CoverEntity>,
addLabel: string,
showDone: boolean,
showLabel: boolean
): TemplateResult | typeof nothing {
const favorites = controller.favorites;
if (!this.editMode && favorites.length === 0) {
return nothing;
}
@@ -370,23 +106,22 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
<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)}
.renderItem=${controller.renderItem}
.deleteLabel=${controller.deleteLabel}
.editMode=${this.editMode ?? false}
.disabled=${this.stateObj.state === UNAVAILABLE}
.isAdmin=${Boolean(this.hass.user?.is_admin)}
.showDone=${showDone}
.addLabel=${this._localizeFavorite(kind, "add")}
.addLabel=${addLabel}
.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}
@favorite-item-action=${controller.handleAction}
@favorite-item-moved=${controller.handleMoved}
@favorite-item-delete=${controller.handleDelete}
@favorite-item-add=${controller.handleAdd}
@favorite-item-done=${controller.handleDone}
></ha-more-info-favorites>
</section>
`;
@@ -400,10 +135,10 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
const supportsPosition = coverSupportsPosition(this.stateObj);
const supportsTiltPosition = coverSupportsTiltPosition(this.stateObj);
const showPositionSection = supportsPosition
? this.editMode || this._favoritePositions.length > 0
? this.editMode || this._positionFavorites.favorites.length > 0
: false;
const showTiltSection = supportsTiltPosition
? this.editMode || this._favoriteTiltPositions.length > 0
? this.editMode || this._tiltFavorites.favorites.length > 0
: false;
const showLabels =
[showPositionSection, showTiltSection].filter(Boolean).length > 1;
@@ -414,18 +149,18 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
<div class="groups">
${supportsPosition
? this._renderKindSection(
"position",
this.hass.localize("ui.card.cover.position"),
this._favoritePositions,
this._positionFavorites,
this._localizeFavorite("position", "add"),
showDoneOnPosition,
showLabels
)
: nothing}
${supportsTiltPosition
? this._renderKindSection(
"tilt",
this.hass.localize("ui.card.cover.tilt_position"),
this._favoriteTiltPositions,
this._tiltFavorites,
this._localizeFavorite("tilt", "add"),
true,
showLabels
)

View File

@@ -7,7 +7,10 @@ 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 {
applyLightFavoriteColor,
resolveLightFavoriteColors,
} from "../../../../data/light";
import type { HomeAssistant } from "../../../../types";
import { showConfirmationDialog } from "../../../generic/show-dialog-box";
import "../ha-more-info-favorites";
@@ -35,11 +38,10 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
protected updated(changedProps: PropertyValues): void {
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);
}
this._favoriteColors = resolveLightFavoriteColors(
this.stateObj,
this.entry.options?.light?.favorite_colors
);
}
}
@@ -53,10 +55,8 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
private _apply(index: number): void {
const favorite = this._favoriteColors[index];
this.hass.callService("light", "turn_on", {
entity_id: this.stateObj.entity_id,
...favorite,
});
applyLightFavoriteColor(this.hass, this.stateObj, favorite);
}
private async _save(newFavoriteColors: LightColor[]): Promise<void> {

View File

@@ -0,0 +1,315 @@
import type {
ReactiveController,
ReactiveControllerHost,
} from "@lit/reactive-element/reactive-controller";
import type { HassEntity } from "home-assistant-js-websocket";
import { html } from "lit";
import type { LitElement } from "lit";
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 { UNAVAILABLE } from "../../../data/entity/entity";
import type {
ExtEntityRegistryEntry,
FavoriteOption,
} 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 type { HaMoreInfoFavorites } from "./ha-more-info-favorites";
export type NumericFavoriteLocalizeKey =
| "set"
| "edit"
| "delete"
| "delete_confirm_title"
| "delete_confirm_text"
| "delete_confirm_action"
| "add"
| "edit_title"
| "add_title";
interface NumericMoreInfoFavoritesControllerConfig<TEntity extends HassEntity> {
getHass: () => HomeAssistant | undefined;
getStateObj: () => TEntity | undefined;
getEntry: () => ExtEntityRegistryEntry | null | undefined;
getEditMode: () => boolean;
domain: "cover" | "valve";
option: Extract<
FavoriteOption,
"favorite_positions" | "favorite_tilt_positions"
>;
defaultFavorites: number[];
getStoredFavorites: (entry: ExtEntityRegistryEntry) => number[] | undefined;
normalizeFavorites: (favorites?: number[]) => number[];
getCurrentValue: (stateObj: TEntity) => number | undefined;
setPositionService: string;
serviceDataKey: string;
localize: (
key: NumericFavoriteLocalizeKey,
values?: Record<string, string | number>
) => string;
getInputLabel: () => string;
inputSuffix: string;
}
export class NumericMoreInfoFavoritesController<
TEntity extends HassEntity,
> implements ReactiveController {
public favorites: number[] = [];
public readonly renderItem: HaMoreInfoFavorites["renderItem"] = (
favorite,
_index,
editMode
) => {
const value = favorite as number;
const stateObj = this._config.getStateObj();
const active =
stateObj !== undefined &&
this._config.getCurrentValue(stateObj) === value;
const label = this._config.localize(editMode ? "edit" : "set", {
value: `${value}%`,
});
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=${stateObj?.state === UNAVAILABLE}
>
${value}%
</ha-control-button>
`;
};
public readonly deleteLabel: HaMoreInfoFavorites["deleteLabel"] = (index) =>
this._config.localize("delete", { number: index + 1 });
public readonly handleAction = async (
ev: HASSDomEvent<HASSDomEvents["favorite-item-action"]>
): Promise<void> => {
ev.stopPropagation();
const hass = this._config.getHass();
if (!hass) {
return;
}
const { action, index } = ev.detail;
if (action === "hold" && hass.user?.is_admin) {
fireEvent(this._host, "toggle-edit-mode", true);
return;
}
if (this._config.getEditMode()) {
await this._editFavorite(index);
return;
}
await this._applyFavorite(index);
};
public readonly handleMoved = async (
ev: HASSDomEvent<HASSDomEvents["favorite-item-moved"]>
): Promise<void> => {
ev.stopPropagation();
await this._move(ev.detail.oldIndex, ev.detail.newIndex);
};
public readonly handleDelete = async (
ev: HASSDomEvent<HASSDomEvents["favorite-item-delete"]>
): Promise<void> => {
ev.stopPropagation();
await this._deleteFavorite(ev.detail.index);
};
public readonly handleAdd = async (
ev: HASSDomEvent<HASSDomEvents["favorite-item-add"]>
): Promise<void> => {
ev.stopPropagation();
await this._addFavorite();
};
public readonly handleDone = (
ev: HASSDomEvent<HASSDomEvents["favorite-item-done"]>
): void => {
ev.stopPropagation();
fireEvent(this._host, "toggle-edit-mode", false);
};
private _lastEntry?: ExtEntityRegistryEntry | null;
private _lastStateObj?: TEntity;
constructor(
private readonly _host: ReactiveControllerHost & LitElement,
private readonly _config: NumericMoreInfoFavoritesControllerConfig<TEntity>
) {
this._host.addController(this);
}
public hostUpdated(): void {
const entry = this._config.getEntry();
const stateObj = this._config.getStateObj();
if (entry === this._lastEntry && stateObj === this._lastStateObj) {
return;
}
this._lastEntry = entry;
this._lastStateObj = stateObj;
this.favorites =
entry && stateObj
? this._config.normalizeFavorites(
this._config.getStoredFavorites(entry) ??
this._config.defaultFavorites
)
: [];
this._host.requestUpdate();
}
private async _saveFavorites(favorites: number[]): Promise<void> {
const hass = this._config.getHass();
const entry = this._config.getEntry();
if (!hass || !entry) {
return;
}
const result = await updateEntityRegistryEntry(hass, entry.entity_id, {
options_domain: this._config.domain,
options: {
...(entry.options?.[this._config.domain] ?? {}),
[this._config.option]: favorites,
},
});
fireEvent(this._host, "entity-entry-updated", result.entity_entry);
}
private async _setFavorites(favorites: number[]): Promise<void> {
this.favorites = this._config.normalizeFavorites(favorites);
this._host.requestUpdate();
await this._saveFavorites(this.favorites);
}
private async _move(index: number, newIndex: number): Promise<void> {
const favorites = this.favorites.concat();
const moved = favorites.splice(index, 1)[0];
favorites.splice(newIndex, 0, moved);
await this._setFavorites(favorites);
}
private async _applyFavorite(index: number): Promise<void> {
const hass = this._config.getHass();
const stateObj = this._config.getStateObj();
const favorite = this.favorites[index];
if (!hass || !stateObj || favorite === undefined) {
return;
}
await hass.callService(
this._config.domain,
this._config.setPositionService,
{
entity_id: stateObj.entity_id,
[this._config.serviceDataKey]: favorite,
}
);
}
private async _promptFavoriteValue(
value?: number
): Promise<number | undefined> {
const hass = this._config.getHass();
if (!hass) {
return undefined;
}
const response = await showPromptDialog(this._host, {
title: this._config.localize(
value === undefined ? "add_title" : "edit_title"
),
inputLabel: this._config.getInputLabel(),
inputType: "number",
inputMin: "0",
inputMax: "100",
inputSuffix: this._config.inputSuffix,
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(): Promise<void> {
const value = await this._promptFavoriteValue();
if (value === undefined) {
return;
}
await this._setFavorites([...this.favorites, value]);
}
private async _editFavorite(index: number): Promise<void> {
const current = this.favorites[index];
if (current === undefined) {
return;
}
const value = await this._promptFavoriteValue(current);
if (value === undefined) {
return;
}
const favorites = [...this.favorites];
favorites[index] = value;
await this._setFavorites(favorites);
}
private async _deleteFavorite(index: number): Promise<void> {
const confirmed = await showConfirmationDialog(this._host, {
destructive: true,
title: this._config.localize("delete_confirm_title"),
text: this._config.localize("delete_confirm_text"),
confirmText: this._config.localize("delete_confirm_action"),
});
if (!confirmed) {
return;
}
await this._setFavorites(
this.favorites.filter((_, itemIndex) => itemIndex !== index)
);
}
}

View File

@@ -1,41 +1,20 @@
import type { PropertyValues, TemplateResult } from "lit";
import type { 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 { customElement, property } from "lit/decorators";
import { UNAVAILABLE } from "../../../../data/entity/entity";
import { DOMAIN_ATTRIBUTES_UNITS } from "../../../../data/entity/entity_attributes";
import type {
ExtEntityRegistryEntry,
ValveEntityOptions,
} from "../../../../data/entity/entity_registry";
import { updateEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../../types";
import type { ValveEntity } from "../../../../data/valve";
import {
DEFAULT_VALVE_FAVORITE_POSITIONS,
normalizeValveFavoritePositions,
} from "../../../../data/valve";
import {
showConfirmationDialog,
showPromptDialog,
} from "../../../generic/show-dialog-box";
import "../ha-more-info-favorites";
import type { HaMoreInfoFavorites } from "../ha-more-info-favorites";
type FavoriteLocalizeKey =
| "set"
| "edit"
| "delete"
| "delete_confirm_title"
| "delete_confirm_text"
| "delete_confirm_action"
| "add"
| "edit_title"
| "add_title";
import {
NumericMoreInfoFavoritesController,
type NumericFavoriteLocalizeKey,
} from "../numeric-more-info-favorites-controller";
@customElement("ha-more-info-valve-favorite-positions")
export class HaMoreInfoValveFavoritePositions extends LitElement {
@@ -47,23 +26,31 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
@property({ attribute: false }) public editMode?: boolean;
@state() private _favoritePositions: number[] = [];
private readonly _favorites =
new NumericMoreInfoFavoritesController<ValveEntity>(this, {
getHass: () => this.hass,
getStateObj: () => this.stateObj,
getEntry: () => this.entry,
getEditMode: () => this.editMode ?? false,
domain: "valve",
option: "favorite_positions",
defaultFavorites: DEFAULT_VALVE_FAVORITE_POSITIONS,
getStoredFavorites: (entry) => entry.options?.valve?.favorite_positions,
normalizeFavorites: normalizeValveFavoritePositions,
getCurrentValue: (stateObj) => {
const current = stateObj.attributes.current_position;
protected updated(changedProps: PropertyValues<this>): void {
if (
(changedProps.has("entry") || changedProps.has("stateObj")) &&
this.entry &&
this.stateObj
) {
this._favoritePositions = normalizeValveFavoritePositions(
this.entry.options?.valve?.favorite_positions ??
DEFAULT_VALVE_FAVORITE_POSITIONS
);
}
}
return current == null ? undefined : Math.round(current);
},
setPositionService: "set_valve_position",
serviceDataKey: "position",
localize: (key, values) => this._localizeFavorite(key, values),
getInputLabel: () => this.hass.localize("ui.card.cover.position"),
inputSuffix: DOMAIN_ATTRIBUTES_UNITS.valve.current_position,
});
private _localizeFavorite(
key: FavoriteLocalizeKey,
key: NumericFavoriteLocalizeKey,
values?: Record<string, string | number>
): string {
return this.hass.localize(
@@ -72,230 +59,17 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
);
}
private _currentValue(): number | undefined {
const current = this.stateObj.attributes.current_position;
return current == null ? undefined : Math.round(current);
}
private async _save(favorite_positions: number[]): Promise<void> {
if (!this.entry) {
return;
}
const currentOptions: ValveEntityOptions = {
...(this.entry.options?.valve ?? {}),
};
currentOptions.favorite_positions = this._favoritePositions;
const result = await updateEntityRegistryEntry(
this.hass,
this.entry.entity_id,
{
options_domain: "valve",
options: {
...currentOptions,
favorite_positions,
},
}
);
fireEvent(this, "entity-entry-updated", result.entity_entry);
}
private async _setFavorites(favorites: number[]): Promise<void> {
const normalized = normalizeValveFavoritePositions(favorites);
this._favoritePositions = normalized;
await this._save(normalized);
}
private _move(index: number, newIndex: number): void {
const favorites = this._favoritePositions.concat();
const moved = favorites.splice(index, 1)[0];
favorites.splice(newIndex, 0, moved);
this._setFavorites(favorites);
}
private _applyFavorite(index: number): void {
const favorite = this._favoritePositions[index];
if (favorite === undefined) {
return;
}
this.hass.callService("valve", "set_valve_position", {
entity_id: this.stateObj.entity_id,
position: favorite,
});
}
private async _promptFavoriteValue(
value?: number
): Promise<number | undefined> {
const response = await showPromptDialog(this, {
title: this._localizeFavorite(
value === undefined ? "add_title" : "edit_title"
),
inputLabel: this.hass.localize("ui.card.valve.position"),
inputType: "number",
inputMin: "0",
inputMax: "100",
inputSuffix: DOMAIN_ATTRIBUTES_UNITS.valve.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(): Promise<void> {
const value = await this._promptFavoriteValue();
if (value === undefined) {
return;
}
await this._setFavorites([...this._favoritePositions, value]);
}
private async _editFavorite(index: number): Promise<void> {
const current = this._favoritePositions[index];
if (current === undefined) {
return;
}
const value = await this._promptFavoriteValue(current);
if (value === undefined) {
return;
}
const updated = [...this._favoritePositions];
updated[index] = value;
await this._setFavorites(updated);
}
private async _deleteFavorite(index: number): Promise<void> {
const confirmed = await showConfirmationDialog(this, {
destructive: true,
title: this._localizeFavorite("delete_confirm_title"),
text: this._localizeFavorite("delete_confirm_text"),
confirmText: this._localizeFavorite("delete_confirm_action"),
});
if (!confirmed) {
return;
}
await this._setFavorites(
this._favoritePositions.filter((_, itemIndex) => itemIndex !== index)
);
}
private _renderFavorite: HaMoreInfoFavorites["renderItem"] = (
favorite,
_index,
editMode
) => {
const value = favorite as number;
const active = this._currentValue() === value;
const label = this._localizeFavorite(editMode ? "edit" : "set", {
value: `${value}%`,
});
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}
>
${value}%
</ha-control-button>
`;
};
private _deleteLabel = (index: number): string =>
this._localizeFavorite("delete", {
number: index + 1,
});
private _handleFavoriteAction = (
ev: HASSDomEvent<HASSDomEvents["favorite-item-action"]>
): void => {
ev.stopPropagation();
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(index);
return;
}
this._applyFavorite(index);
};
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._deleteFavorite(ev.detail.index);
};
private _handleFavoriteAdd = (
ev: HASSDomEvent<HASSDomEvents["favorite-item-add"]>
): void => {
ev.stopPropagation();
this._addFavorite();
};
private _handleFavoriteDone = (
ev: HASSDomEvent<HASSDomEvents["favorite-item-done"]>
): void => {
ev.stopPropagation();
fireEvent(this, "toggle-edit-mode", false);
};
private _renderSection(): TemplateResult | typeof nothing {
if (!this.editMode && this._favoritePositions.length === 0) {
if (!this.editMode && this._favorites.favorites.length === 0) {
return nothing;
}
return html`
<section class="group">
<ha-more-info-favorites
.items=${this._favoritePositions}
.renderItem=${this._renderFavorite}
.deleteLabel=${this._deleteLabel}
.items=${this._favorites.favorites}
.renderItem=${this._favorites.renderItem}
.deleteLabel=${this._favorites.deleteLabel}
.editMode=${this.editMode ?? false}
.disabled=${this.stateObj.state === UNAVAILABLE}
.isAdmin=${Boolean(this.hass.user?.is_admin)}
@@ -304,11 +78,11 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
.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}
@favorite-item-action=${this._favorites.handleAction}
@favorite-item-moved=${this._favorites.handleMoved}
@favorite-item-delete=${this._favorites.handleDelete}
@favorite-item-add=${this._favorites.handleAdd}
@favorite-item-done=${this._favorites.handleDone}
></ha-more-info-favorites>
</section>
`;

View File

@@ -26,10 +26,10 @@ import {
import type { LightColor, LightEntity } from "../../data/light";
import {
LightColorMode,
computeDefaultFavoriteColors,
lightSupportsFavoriteColors,
lightSupportsColor,
lightSupportsColorMode,
lightSupportsFavoriteColors,
resolveLightFavoriteColors,
} from "../../data/light";
import type { ValveEntity } from "../../data/valve";
import {
@@ -256,9 +256,10 @@ const lightFavoritesHandler: FavoritesDialogHandler = {
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 favorites: LightColor[] = resolveLightFavoriteColors(
lightStateObj,
entry.options?.light?.favorite_colors
);
const favoriteTypes = [
...new Set(favorites.map((item) => Object.keys(item)[0])),
@@ -268,6 +269,7 @@ const lightFavoritesHandler: FavoritesDialogHandler = {
(candidate) =>
candidate.entity_id !== lightStateObj.entity_id &&
computeStateDomain(candidate) === "light" &&
favoriteTypes.length > 0 &&
favoriteTypes.every((type) =>
type === "color_temp_kelvin"
? lightSupportsColorMode(

View File

@@ -0,0 +1,144 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type {
ReactiveController,
ReactiveControllerHost,
} from "@lit/reactive-element/reactive-controller";
import type { LitElement } from "lit";
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import {
getExtendedEntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../../types";
interface FavoritesEntityRegistryEntryControllerConfig {
getHass: () => HomeAssistant | undefined;
getEntityId: () => string | undefined;
getEntry: () => ExtEntityRegistryEntry | null | undefined;
setEntry: (entry: ExtEntityRegistryEntry | null | undefined) => void;
}
export const createFavoritesEntityRegistryEntryController = (
host: ReactiveControllerHost & LitElement,
config: FavoritesEntityRegistryEntryControllerConfig
): FavoritesEntityRegistryEntryController =>
new FavoritesEntityRegistryEntryController(host, config);
export class FavoritesEntityRegistryEntryController implements ReactiveController {
private _unsubEntityRegistry?: UnsubscribeFunc;
private _subscribedEntityId?: string;
private _subscribedConnection?: HomeAssistant["connection"];
constructor(
private readonly _host: ReactiveControllerHost & LitElement,
private readonly _config: FavoritesEntityRegistryEntryControllerConfig
) {
this._host.addController(this);
}
public hostConnected(): void {
this._refreshEntitySubscription();
}
public hostUpdated(): void {
this._refreshEntitySubscription();
}
public hostDisconnected(): void {
this._unsubscribeEntityRegistry();
}
private _refreshEntitySubscription(): void {
this._ensureEntitySubscription().catch(() => undefined);
}
private _setEntry(entry: ExtEntityRegistryEntry | null | undefined): void {
if (this._config.getEntry() === entry) {
return;
}
this._config.setEntry(entry);
}
private _unsubscribeEntityRegistry(): void {
if (this._unsubEntityRegistry) {
this._unsubEntityRegistry();
this._unsubEntityRegistry = undefined;
}
}
private async _loadEntityEntry(entityId: string): Promise<void> {
const hass = this._config.getHass();
if (!hass) {
return;
}
try {
const entry = await getExtendedEntityRegistryEntry(hass, entityId);
if (this._config.getEntityId() === entityId) {
this._setEntry(entry);
}
} catch (_err) {
if (this._config.getEntityId() === entityId) {
this._setEntry(null);
}
}
}
private async _subscribeEntityEntry(entityId: string): Promise<void> {
this._unsubscribeEntityRegistry();
await this._loadEntityEntry(entityId);
try {
this._unsubEntityRegistry = subscribeEntityRegistry(
this._config.getHass()!.connection,
async (entries) => {
if (this._config.getEntityId() !== entityId) {
return;
}
if (entries.some((entry) => entry.entity_id === entityId)) {
await this._loadEntityEntry(entityId);
return;
}
this._setEntry(null);
}
);
} catch (_err) {
this._unsubEntityRegistry = undefined;
}
}
private async _ensureEntitySubscription(): Promise<void> {
const hass = this._config.getHass();
const entityId = this._config.getEntityId();
const connection = hass?.connection;
if (!hass || !entityId || !connection) {
this._unsubscribeEntityRegistry();
this._subscribedEntityId = undefined;
this._subscribedConnection = undefined;
this._setEntry(undefined);
return;
}
if (
this._subscribedEntityId === entityId &&
this._subscribedConnection === connection &&
this._unsubEntityRegistry
) {
return;
}
this._subscribedEntityId = entityId;
this._subscribedConnection = connection;
await this._subscribeEntityEntry(entityId);
}
}

View File

@@ -1,26 +1,24 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state, query } from "lit/decorators";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { computeDomain } from "../../../common/entity/compute_domain";
import { UNAVAILABLE } from "../../../data/entity/entity";
import {
computeDefaultFavoriteColors,
applyLightFavoriteColor,
type LightEntity,
type LightColor,
lightSupportsFavoriteColors,
resolveLightFavoriteColors,
} from "../../../data/light";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { createFavoritesEntityRegistryEntryController } from "./common/favorites-entity-registry-entry-controller";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
LightColorFavoritesCardFeatureConfig,
LovelaceCardFeatureContext,
} from "./types";
import {
type EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity/entity_registry";
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
import "../../../dialogs/more-info/components/lights/ha-favorite-color-button";
import { actionHandler } from "../common/directives/action-handler-directive";
import { debounce } from "../../../common/util/debounce";
@@ -48,7 +46,7 @@ class HuiLightColorFavoritesCardFeature
@state() private _config?: LightColorFavoritesCardFeatureConfig;
@state() private _entry?: EntityRegistryEntry | null;
@state() private _entry?: ExtEntityRegistryEntry | null;
@state() private _favoriteColors: LightColor[] = [];
@@ -58,46 +56,29 @@ class HuiLightColorFavoritesCardFeature
private _resizeObserver?: ResizeObserver;
private _unsubEntityRegistry?: UnsubscribeFunc;
constructor() {
super();
createFavoritesEntityRegistryEntryController(this, {
getHass: () => this.hass,
getEntityId: () => this.context?.entity_id,
getEntry: () => this._entry,
setEntry: (entry) => {
this._entry = entry;
},
});
}
public connectedCallback() {
super.connectedCallback();
this._subscribeEntityEntry();
this.updateComplete.then(() => this._attachObserver());
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeEntityRegistry();
this._resizeObserver?.disconnect();
}
private _unsubscribeEntityRegistry() {
if (this._unsubEntityRegistry) {
this._unsubEntityRegistry();
this._unsubEntityRegistry = undefined;
}
}
private _subscribeEntityEntry() {
if (this.hass && this.context?.entity_id) {
const id = this.context.entity_id;
try {
this._unsubEntityRegistry = subscribeEntityRegistry(
this.hass!.connection,
(entries) => {
const entry = entries.find((e) => e.entity_id === id);
if (entry) {
this._entry = entry;
}
}
);
} catch (_e) {
this._entry = null;
}
}
}
private _measure() {
const w = this._container.clientWidth;
const pillMin = 32 + 8;
@@ -121,24 +102,14 @@ class HuiLightColorFavoritesCardFeature
}
protected updated(changedProps: PropertyValues): void {
if (changedProps.has("context")) {
this._unsubscribeEntityRegistry();
this._subscribeEntityEntry();
}
if (changedProps.has("_entry") || changedProps.has("_maxVisible")) {
if (this._entry) {
if (this._entry.options?.light?.favorite_colors) {
this._favoriteColors =
this._entry.options.light.favorite_colors.slice(
0,
this._maxVisible
);
} else if (this._stateObj) {
this._favoriteColors = computeDefaultFavoriteColors(
this._stateObj
).slice(0, this._maxVisible);
}
this._favoriteColors = this._stateObj
? resolveLightFavoriteColors(
this._stateObj,
this._entry.options?.light?.favorite_colors
).slice(0, this._maxVisible)
: [];
}
}
}
@@ -199,10 +170,7 @@ class HuiLightColorFavoritesCardFeature
const index = (ev.target! as any).index!;
const favorite = this._favoriteColors[index];
this.hass!.callService("light", "turn_on", {
entity_id: this._stateObj!.entity_id,
...favorite,
});
applyLightFavoriteColor(this.hass!, this._stateObj!, favorite);
}
static get styles() {

View File

@@ -1,4 +1,4 @@
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement } from "lit";
import { property, state } from "lit/decorators";
@@ -11,12 +11,9 @@ import type { LocalizeKeys } from "../../../common/translations/localize";
import "../../../components/ha-control-select";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
import {
getExtendedEntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../types";
import type { LovelaceCardFeature } from "../types";
import { createFavoritesEntityRegistryEntryController } from "./common/favorites-entity-registry-entry-controller";
import { cardFeatureStyles } from "./common/card-feature-styles";
import type {
LovelaceCardFeatureConfig,
@@ -87,14 +84,21 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
@state() protected _currentPosition?: number;
private _unsubEntityRegistry?: UnsubscribeFunc;
private _subscribedEntityId?: string;
private _subscribedConnection?: HomeAssistant["connection"];
protected abstract get _definition(): NumericFavoriteCardFeatureDefinition<TEntity>;
constructor() {
super();
createFavoritesEntityRegistryEntryController(this, {
getHass: () => this.hass,
getEntityId: () => this.context?.entity_id,
getEntry: () => this._entry,
setEntry: (entry) => {
this._entry = entry;
},
});
}
protected get _stateObj(): TEntity | undefined {
if (!this.hass || !this.context?.entity_id) {
return undefined;
@@ -103,16 +107,6 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
return this.hass.states[this.context.entity_id] as TEntity | undefined;
}
public connectedCallback() {
super.connectedCallback();
this._refreshEntitySubscription();
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribeEntityRegistry();
}
public setConfig(config: LovelaceCardFeatureConfig): void {
if (!config) {
throw new Error("Invalid configuration");
@@ -139,103 +133,6 @@ export abstract class HuiNumericFavoriteCardFeatureBase<
);
}
}
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(

View File

@@ -1,41 +1,12 @@
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";
import { customElement } from "lit/decorators";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import type { CoverPositionFavoriteCardFeatureConfig } from "../../card-features/types";
import { HuiFavoriteCardFeatureEditorBase } from "./hui-favorite-card-feature-editor-base";
@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>
`;
}
export class HuiCoverPositionFavoriteCardFeatureEditor extends HuiFavoriteCardFeatureEditorBase<CoverPositionFavoriteCardFeatureConfig> {
protected readonly _descriptionKey =
"ui.panel.lovelace.editor.features.types.cover-position-favorite.description" satisfies LocalizeKeys;
}
declare global {

View File

@@ -1,41 +1,12 @@
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";
import { customElement } from "lit/decorators";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import type { CoverTiltFavoriteCardFeatureConfig } from "../../card-features/types";
import { HuiFavoriteCardFeatureEditorBase } from "./hui-favorite-card-feature-editor-base";
@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>
`;
}
export class HuiCoverTiltFavoriteCardFeatureEditor extends HuiFavoriteCardFeatureEditorBase<CoverTiltFavoriteCardFeatureConfig> {
protected readonly _descriptionKey =
"ui.panel.lovelace.editor.features.types.cover-tilt-favorite.description" satisfies LocalizeKeys;
}
declare global {

View File

@@ -0,0 +1,41 @@
import { html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import "../../../../components/ha-alert";
import type { HomeAssistant } from "../../../../types";
import type {
LovelaceCardFeatureConfig,
LovelaceCardFeatureContext,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
export abstract class HuiFavoriteCardFeatureEditorBase<
TConfig extends LovelaceCardFeatureConfig,
>
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: TConfig;
protected abstract readonly _descriptionKey: LocalizeKeys;
public setConfig(config: TConfig): void {
this._config = config;
}
protected render() {
if (!this.hass || !this._config) {
return nothing;
}
return html`
<ha-alert alert-type="info">
${this.hass.localize(this._descriptionKey)}
</ha-alert>
`;
}
}

View File

@@ -1,41 +1,12 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../../../components/ha-alert";
import type { HomeAssistant } from "../../../../types";
import type {
LovelaceCardFeatureContext,
ValvePositionFavoriteCardFeatureConfig,
} from "../../card-features/types";
import type { LovelaceCardFeatureEditor } from "../../types";
import { customElement } from "lit/decorators";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import type { ValvePositionFavoriteCardFeatureConfig } from "../../card-features/types";
import { HuiFavoriteCardFeatureEditorBase } from "./hui-favorite-card-feature-editor-base";
@customElement("hui-valve-position-favorite-card-feature-editor")
export class HuiValvePositionFavoriteCardFeatureEditor
extends LitElement
implements LovelaceCardFeatureEditor
{
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public context?: LovelaceCardFeatureContext;
@state() private _config?: ValvePositionFavoriteCardFeatureConfig;
public setConfig(config: ValvePositionFavoriteCardFeatureConfig): 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.valve-position-favorite.description"
)}
</ha-alert>
`;
}
export class HuiValvePositionFavoriteCardFeatureEditor extends HuiFavoriteCardFeatureEditorBase<ValvePositionFavoriteCardFeatureConfig> {
protected readonly _descriptionKey =
"ui.panel.lovelace.editor.features.types.valve-position-favorite.description" satisfies LocalizeKeys;
}
declare global {