Compare commits

...

4 Commits

Author SHA1 Message Date
Aidan Timson 61693f9c59 Migrate section and view editors to conditional visibility 2026-07-21 10:39:54 +01:00
karwosts a25aa656a3 Replace feature magic numbers in media-player-model (#53214) 2026-07-21 10:04:39 +02:00
Simon Lamon 88f244b9df Replace the memoize call to fullcalendar (#53215)
Replace the memoize one
2026-07-21 10:04:13 +02:00
Artur Pragacz ad3389fd4d Use registry for Alexa name override (#53216)
Use registry aliases for Alexa
2026-07-21 09:51:01 +02:00
9 changed files with 189 additions and 133 deletions
+40 -14
View File
@@ -1,19 +1,23 @@
import { consume, type ContextType } from "@lit/context";
import { initialState } from "@lit/task";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import type { HassEntity } from "home-assistant-js-websocket";
import { AsyncValueTask } from "../../common/controllers/async-value-task";
import { consumeEntityState } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import {
configContext,
connectionContext,
entitiesContext,
} from "../../data/context";
import { entityIcon } from "../../data/icons";
import type { IconSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-icon-picker";
import "../ha-state-icon";
@customElement("ha-selector-icon")
export class HaIconSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: IconSelector;
@property() public value?: string;
@@ -30,10 +34,21 @@ export class HaIconSelector extends LitElement {
icon_entity?: string;
};
private get _stateObj(): HassEntity | undefined {
const iconEntity = this.context?.icon_entity;
return iconEntity ? this.hass.states[iconEntity] : undefined;
}
@state()
@consumeEntityState({ entityIdPath: ["context", "icon_entity"] })
private _stateObj?: HassEntity;
@state()
@consume({ context: entitiesContext, subscribe: true })
private _entities?: ContextType<typeof entitiesContext>;
@state()
@consume({ context: configContext, subscribe: true })
private _config?: ContextType<typeof configContext>;
@state()
@consume({ context: connectionContext, subscribe: true })
private _connection?: ContextType<typeof connectionContext>;
private _placeholderTask = new AsyncValueTask(this, {
task: ([
@@ -44,19 +59,31 @@ export class HaIconSelector extends LitElement {
connection,
stateObj,
]) => {
if (placeholder || attributeIcon || !stateObj) {
if (
placeholder ||
attributeIcon ||
!entities ||
!config ||
!connection ||
!stateObj
) {
return initialState;
}
return entityIcon(entities, config, connection, stateObj);
return entityIcon(
entities,
config.config,
connection.connection,
stateObj
);
},
args: () => {
const stateObj = this._stateObj;
return [
this.selector.icon?.placeholder,
stateObj?.attributes.icon,
this.hass.entities,
this.hass.config,
this.hass.connection,
this._entities,
this._config,
this._connection,
stateObj,
] as const;
},
@@ -72,7 +99,6 @@ export class HaIconSelector extends LitElement {
return html`
<ha-icon-picker
.hass=${this.hass}
.label=${this.label}
.value=${this.value}
.required=${this.required}
+1 -2
View File
@@ -1,4 +1,3 @@
import { memoize } from "@fullcalendar/core/internal";
import { setHours, setMinutes } from "date-fns";
import type { HassConfig } from "home-assistant-js-websocket";
import memoizeOne from "memoize-one";
@@ -411,7 +410,7 @@ export type BackupType = "automatic" | "manual" | "app_update";
const BACKUP_TYPE_ORDER: BackupType[] = ["automatic", "app_update", "manual"];
export const getBackupTypes = memoize((isHassio: boolean) =>
export const getBackupTypes = memoizeOne((isHassio: boolean) =>
isHassio
? BACKUP_TYPE_ORDER
: BACKUP_TYPE_ORDER.filter((type) => type !== "app_update")
@@ -179,6 +179,16 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
const anyExposed = uiExposed || manExposedAlexa || manExposedGoogle;
const exposedToAlexa =
showAssistants.includes("cloud.alexa") &&
(alexaManual ? manExposedAlexa : this.exposed["cloud.alexa"]);
const exposedToGoogle =
showAssistants.includes("cloud.google_assistant") &&
(googleManual
? manExposedGoogle
: this.exposed["cloud.google_assistant"]);
const exposedToAssist = this.exposed.conversation;
return html`
<ha-md-list-item>
<h3 slot="headline">
@@ -275,7 +285,24 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
</h3>
<p class="description">
${this.hass.localize("ui.dialogs.voice-settings.aliases_description")}
${[
this.hass.localize("ui.dialogs.voice-settings.aliases_description"),
exposedToAlexa &&
this.hass.localize(
"ui.dialogs.voice-settings.aliases_description_alexa"
),
exposedToGoogle &&
this.hass.localize(
"ui.dialogs.voice-settings.aliases_description_google"
),
exposedToAssist &&
(exposedToAlexa || exposedToGoogle) &&
this.hass.localize(
"ui.dialogs.voice-settings.aliases_description_assist"
),
]
.filter(Boolean)
.join(" ")}
</p>
${
@@ -137,7 +137,6 @@ export class HuiDialogEditSection
case "tab-settings":
content = html`
<hui-section-settings-editor
.hass=${this.hass}
.config=${this._config}
.viewConfig=${this._viewConfig}
@value-changed=${this._configChanged}
@@ -2,6 +2,7 @@ import { mdiPalette } from "@mdi/js";
import { LitElement, html } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import type { LocalizeFunc } from "../../../../common/translations/localize";
import "../../../../components/ha-form/ha-form";
@@ -15,7 +16,6 @@ import {
type LovelaceSectionRawConfig,
} from "../../../../data/lovelace/config/section";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
interface SettingsData {
column_span?: number;
@@ -27,14 +27,15 @@ interface SettingsData {
@customElement("hui-section-settings-editor")
export class HuiDialogEditSection extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public config!: LovelaceSectionRawConfig;
@property({ attribute: false }) public viewConfig!: LovelaceViewConfig;
private _schema = memoizeOne(
(maxColumns: number, backgroundEnabled: boolean, localize: LocalizeFunc) =>
(maxColumns: number, localize: LocalizeFunc) =>
[
{
name: "column_span",
@@ -50,48 +51,45 @@ export class HuiDialogEditSection extends LitElement {
name: "background_enabled",
selector: { boolean: {} },
},
...(backgroundEnabled
? ([
{
name: "background",
type: "expandable",
flatten: true,
expanded: true,
iconPath: mdiPalette,
schema: [
{
name: "background_color",
selector: {
ui_color: {
extra_options: [
{
value: "default",
label: localize(
"ui.panel.lovelace.editor.edit_section.settings.background_color_default"
),
display_color:
"var(--ha-section-background-color, var(--secondary-background-color))",
},
],
},
{
name: "background",
type: "expandable",
flatten: true,
expanded: true,
hidden: { field: "background_enabled", value: false },
iconPath: mdiPalette,
schema: [
{
name: "background_color",
selector: {
ui_color: {
extra_options: [
{
value: "default",
label: localize(
"ui.panel.lovelace.editor.edit_section.settings.background_color_default"
),
display_color:
"var(--ha-section-background-color, var(--secondary-background-color))",
},
},
{
name: "background_opacity",
selector: {
number: {
min: 0,
max: 100,
step: 1,
unit_of_measurement: "%",
mode: "slider",
},
},
},
],
],
},
},
] as const satisfies readonly HaFormSchema[])
: []),
},
{
name: "background_opacity",
selector: {
number: {
min: 0,
max: 100,
step: 1,
unit_of_measurement: "%",
mode: "slider",
},
},
},
],
},
{
name: "theme",
selector: {
@@ -116,13 +114,11 @@ export class HuiDialogEditSection extends LitElement {
const schema = this._schema(
this.viewConfig.max_columns || 4,
backgroundEnabled,
this.hass.localize
this._localize
);
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.computeLabel=${this._computeLabel}
@@ -135,14 +131,14 @@ export class HuiDialogEditSection extends LitElement {
private _computeLabel = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass.localize(
this._localize(
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}`
);
private _computeHelper = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) =>
this.hass.localize(
this._localize(
`ui.panel.lovelace.editor.edit_section.settings.${schema.name}_helper`
) || "";
@@ -170,7 +170,6 @@ export class HuiDialogEditView extends DirtyStateProviderMixin<LovelaceViewConfi
content = html`
<hui-view-editor
.isNew=${this._params.viewIndex === undefined}
.hass=${this.hass}
.config=${this._config}
@view-config-changed=${this._viewConfigChanged}
></hui-view-editor>
@@ -1,6 +1,7 @@
import { html, LitElement, nothing } from "lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
import { fireEvent } from "../../../../common/dom/fire_event";
import { slugify } from "../../../../common/string/slugify";
import type { LocalizeFunc } from "../../../../common/translations/localize";
@@ -10,7 +11,6 @@ import type {
SchemaUnion,
} from "../../../../components/ha-form/types";
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
import type { HomeAssistant } from "../../../../types";
import {
MASONRY_VIEW_LAYOUT,
SECTIONS_VIEW_LAYOUT,
@@ -33,7 +33,8 @@ const INTEGER_REGEX = /^[0-9]+$/;
@customElement("hui-view-editor")
export class HuiViewEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public isNew = false;
@@ -44,7 +45,7 @@ export class HuiViewEditor extends LitElement {
private _suggestedPath = false;
private _schema = memoizeOne(
(localize: LocalizeFunc, viewType: string) =>
(localize: LocalizeFunc) =>
[
{
name: "type",
@@ -87,41 +88,42 @@ export class HuiViewEditor extends LitElement {
boolean: {},
},
},
...(viewType === SECTIONS_VIEW_LAYOUT
? ([
{
name: "section_specifics",
type: "expandable",
flatten: true,
expanded: true,
schema: [
{
name: "max_columns",
selector: {
number: {
min: 1,
max: 10,
mode: "slider",
slider_ticks: true,
},
},
},
{
name: "dense_section_placement",
selector: {
boolean: {},
},
},
{
name: "top_margin",
selector: {
boolean: {},
},
},
],
{
name: "section_specifics",
type: "expandable",
flatten: true,
expanded: true,
hidden: {
field: "type",
operator: "not_eq",
value: SECTIONS_VIEW_LAYOUT,
},
schema: [
{
name: "max_columns",
selector: {
number: {
min: 1,
max: 10,
mode: "slider",
slider_ticks: true,
},
},
] as const satisfies HaFormSchema[])
: []),
},
{
name: "dense_section_placement",
selector: {
boolean: {},
},
},
{
name: "top_margin",
selector: {
boolean: {},
},
},
],
},
] as const satisfies HaFormSchema[]
);
@@ -134,12 +136,6 @@ export class HuiViewEditor extends LitElement {
}
protected render() {
if (!this.hass) {
return nothing;
}
const schema = this._schema(this.hass.localize, this._type);
const data = {
...this._config,
type: this._type,
@@ -155,9 +151,8 @@ export class HuiViewEditor extends LitElement {
return html`
<ha-form
.hass=${this.hass}
.data=${data}
.schema=${schema}
.schema=${this._schema(this._localize)}
.computeLabel=${this._computeLabel}
.computeHelper=${this._computeHelper}
.computeError=${this._computeError}
@@ -207,14 +202,14 @@ export class HuiViewEditor extends LitElement {
}
private _computeError = (error: string) =>
this.hass.localize(`ui.panel.lovelace.editor.edit_view.${error}`) || error;
this._localize(`ui.panel.lovelace.editor.edit_view.${error}`) || error;
private _computeLabel = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
switch (schema.name) {
case "path":
return this.hass!.localize("ui.panel.lovelace.editor.card.generic.url");
return this._localize("ui.panel.lovelace.editor.card.generic.url");
case "type":
case "show_icon_and_title":
case "subview":
@@ -222,11 +217,11 @@ export class HuiViewEditor extends LitElement {
case "dense_section_placement":
case "top_margin":
case "section_specifics":
return this.hass.localize(
return this._localize(
`ui.panel.lovelace.editor.edit_view.${schema.name}`
);
default:
return this.hass!.localize(
return this._localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
);
}
@@ -241,7 +236,7 @@ export class HuiViewEditor extends LitElement {
case "subview":
case "dense_section_placement":
case "top_margin":
return this.hass.localize(
return this._localize(
`ui.panel.lovelace.editor.edit_view.${schema.name}_helper`
);
+5 -2
View File
@@ -2025,9 +2025,12 @@
"voice-settings": {
"expose_header": "Expose",
"aliases_header": "Aliases",
"aliases_description": "Aliases are alternative names to call your entity. Only supported by Assist and Google Assistant.",
"aliases_description": "Aliases are the names voice assistants use for this entity.",
"aliases_description_assist": "Assist uses all aliases equally.",
"aliases_description_alexa": "Amazon Alexa uses only the first alias.",
"aliases_description_google": "Google Assistant uses the first alias as the main name and the rest as alternatives.",
"aliases_no_unique_id": "Aliases are not supported for entities without a unique ID. See the {faq_link} for more detail.",
"entity_name_alias_description": "Default name. Disable it if you want your voice assistants to ignore it and just use aliases.",
"entity_name_alias_description": "Default name. When enabled, it is used as the first alias.",
"ask_pin": "Ask for PIN",
"manual_config": "Managed in configuration.yaml",
"unsupported": "Unsupported",
+25 -13
View File
@@ -1,6 +1,9 @@
import type { HassEntity } from "home-assistant-js-websocket";
import { supportsFeature } from "../common/entity/supports-feature";
import { cleanupMediaTitle } from "../data/media-player";
import {
cleanupMediaTitle,
MediaPlayerEntityFeature,
} from "../data/media-player";
import type { HomeAssistant } from "../types";
export default class MediaPlayerEntity {
@@ -75,51 +78,60 @@ export default class MediaPlayerEntity {
}
get supportsPause() {
return supportsFeature(this.stateObj, 1);
return supportsFeature(this.stateObj, MediaPlayerEntityFeature.PAUSE);
}
get supportsVolumeSet() {
return supportsFeature(this.stateObj, 4);
return supportsFeature(this.stateObj, MediaPlayerEntityFeature.VOLUME_SET);
}
get supportsVolumeMute() {
return supportsFeature(this.stateObj, 8);
return supportsFeature(this.stateObj, MediaPlayerEntityFeature.VOLUME_MUTE);
}
get supportsPreviousTrack() {
return supportsFeature(this.stateObj, 16);
return supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.PREVIOUS_TRACK
);
}
get supportsNextTrack() {
return supportsFeature(this.stateObj, 32);
return supportsFeature(this.stateObj, MediaPlayerEntityFeature.NEXT_TRACK);
}
get supportsTurnOn() {
return supportsFeature(this.stateObj, 128);
return supportsFeature(this.stateObj, MediaPlayerEntityFeature.TURN_ON);
}
get supportsTurnOff() {
return supportsFeature(this.stateObj, 256);
return supportsFeature(this.stateObj, MediaPlayerEntityFeature.TURN_OFF);
}
get supportsPlayMedia() {
return supportsFeature(this.stateObj, 512);
return supportsFeature(this.stateObj, MediaPlayerEntityFeature.PLAY_MEDIA);
}
get supportsVolumeButtons() {
return supportsFeature(this.stateObj, 1024);
return supportsFeature(this.stateObj, MediaPlayerEntityFeature.VOLUME_STEP);
}
get supportsSelectSource() {
return supportsFeature(this.stateObj, 2048);
return supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.SELECT_SOURCE
);
}
get supportsSelectSoundMode() {
return supportsFeature(this.stateObj, 65536);
return supportsFeature(
this.stateObj,
MediaPlayerEntityFeature.SELECT_SOUND_MODE
);
}
get supportsPlay() {
return supportsFeature(this.stateObj, 16384);
return supportsFeature(this.stateObj, MediaPlayerEntityFeature.PLAY);
}
get primaryTitle() {