Merge branch 'rc'

This commit is contained in:
Bram Kragten 2025-05-09 12:11:58 +02:00
commit 7f6ce97199
15 changed files with 204 additions and 63 deletions

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20250507.0" version = "20250509.0"
license = "Apache-2.0" license = "Apache-2.0"
license-files = ["LICENSE*"] license-files = ["LICENSE*"]
description = "The Home Assistant frontend" description = "The Home Assistant frontend"

View File

@ -90,7 +90,7 @@ export class HaDialog extends DialogBase {
} }
.mdc-dialog__actions { .mdc-dialog__actions {
justify-content: var(--justify-action-buttons, flex-end); justify-content: var(--justify-action-buttons, flex-end);
padding-bottom: max(env(safe-area-inset-bottom), 24px); padding: 12px 24px max(env(safe-area-inset-bottom), 12px) 24px;
} }
.mdc-dialog__actions span:nth-child(1) { .mdc-dialog__actions span:nth-child(1) {
flex: var(--secondary-action-button-flex, unset); flex: var(--secondary-action-button-flex, unset);
@ -107,9 +107,6 @@ export class HaDialog extends DialogBase {
.mdc-dialog__title:has(span) { .mdc-dialog__title:has(span) {
padding: 12px 12px 0; padding: 12px 12px 0;
} }
.mdc-dialog__actions {
padding: 12px 24px 12px 24px;
}
.mdc-dialog__title::before { .mdc-dialog__title::before {
content: unset; content: unset;
} }

View File

@ -83,7 +83,7 @@ export class HaServiceControl extends LitElement {
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@property({ type: Boolean, reflect: true }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@property({ attribute: "show-advanced", type: Boolean }) public showAdvanced = @property({ attribute: "show-advanced", type: Boolean }) public showAdvanced =
false; false;
@ -895,6 +895,9 @@ export class HaServiceControl extends LitElement {
ha-settings-row { ha-settings-row {
padding: var(--service-control-padding, 0 16px); padding: var(--service-control-padding, 0 16px);
} }
ha-settings-row[narrow] {
padding-bottom: 8px;
}
ha-settings-row { ha-settings-row {
--settings-row-content-width: 100%; --settings-row-content-width: 100%;
--settings-row-prefix-display: contents; --settings-row-prefix-display: contents;
@ -916,7 +919,7 @@ export class HaServiceControl extends LitElement {
margin: var(--service-control-padding, 0 16px); margin: var(--service-control-padding, 0 16px);
padding: 16px 0; padding: 16px 0;
} }
:host([hidePicker]) p { :host([hide-picker]) p {
padding-top: 0; padding-top: 0;
} }
.checkbox-spacer { .checkbox-spacer {

View File

@ -24,9 +24,10 @@ import {
customElement, customElement,
eventOptions, eventOptions,
property, property,
state,
query, query,
state,
} from "lit/decorators"; } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { storage } from "../common/decorators/storage"; import { storage } from "../common/decorators/storage";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
@ -45,13 +46,13 @@ import { haStyleScrollbar } from "../resources/styles";
import type { HomeAssistant, PanelInfo, Route } from "../types"; import type { HomeAssistant, PanelInfo, Route } from "../types";
import "./ha-icon"; import "./ha-icon";
import "./ha-icon-button"; import "./ha-icon-button";
import "./ha-md-list";
import "./ha-md-list-item";
import type { HaMdListItem } from "./ha-md-list-item";
import "./ha-menu-button"; import "./ha-menu-button";
import "./ha-sortable"; import "./ha-sortable";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "./user/ha-user-badge"; import "./user/ha-user-badge";
import "./ha-md-list";
import "./ha-md-list-item";
import type { HaMdListItem } from "./ha-md-list-item";
const SHOW_AFTER_SPACER = ["config", "developer-tools"]; const SHOW_AFTER_SPACER = ["config", "developer-tools"];
@ -407,6 +408,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
// prettier-ignore // prettier-ignore
return html` return html`
<ha-sortable .disabled=${!this.editMode} draggable-selector=".draggable" @item-moved=${this._panelMoved}>
<ha-md-list <ha-md-list
class="ha-scrollbar" class="ha-scrollbar"
@focusin=${this._listboxFocusIn} @focusin=${this._listboxFocusIn}
@ -421,10 +423,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
${this._renderPanels(afterSpacer, selectedPanel)} ${this._renderPanels(afterSpacer, selectedPanel)}
${this._renderExternalConfiguration()} ${this._renderExternalConfiguration()}
</ha-md-list> </ha-md-list>
</ha-sortable>
`; `;
} }
private _renderPanels(panels: PanelInfo[], selectedPanel: string) { private _renderPanels(
panels: PanelInfo[],
selectedPanel: string,
sortable = false
) {
return panels.map((panel) => return panels.map((panel) =>
this._renderPanel( this._renderPanel(
panel.url_path, panel.url_path,
@ -437,17 +444,26 @@ class HaSidebar extends SubscribeMixin(LitElement) {
: panel.url_path in PANEL_ICONS : panel.url_path in PANEL_ICONS
? PANEL_ICONS[panel.url_path] ? PANEL_ICONS[panel.url_path]
: undefined, : undefined,
selectedPanel selectedPanel,
sortable
) )
); );
} }
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
return html`
${this._renderPanels(beforeSpacer, selectedPanel, true)}
${this._renderSpacer()}${this._renderHiddenPanels()}
`;
}
private _renderPanel( private _renderPanel(
urlPath: string, urlPath: string,
title: string | null, title: string | null,
icon: string | null | undefined, icon: string | null | undefined,
iconPath: string | null | undefined, iconPath: string | null | undefined,
selectedPanel: string selectedPanel: string,
sortable = false
) { ) {
return urlPath === "config" return urlPath === "config"
? this._renderConfiguration(title, selectedPanel) ? this._renderConfiguration(title, selectedPanel)
@ -455,7 +471,10 @@ class HaSidebar extends SubscribeMixin(LitElement) {
<ha-md-list-item <ha-md-list-item
.href=${this.editMode ? undefined : `/${urlPath}`} .href=${this.editMode ? undefined : `/${urlPath}`}
type="link" type="link"
class=${selectedPanel === urlPath ? "selected" : ""} class=${classMap({
selected: selectedPanel === urlPath,
draggable: this.editMode && sortable,
})}
@mouseenter=${this._itemMouseEnter} @mouseenter=${this._itemMouseEnter}
@mouseleave=${this._itemMouseLeave} @mouseleave=${this._itemMouseLeave}
> >
@ -496,15 +515,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
this._panelOrder = panelOrder; this._panelOrder = panelOrder;
} }
private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) {
return html`
<ha-sortable .disabled=${!this.editMode} @item-moved=${this._panelMoved}
><div>${this._renderPanels(beforeSpacer, selectedPanel)}</div>
</ha-sortable>
${this._renderSpacer()}${this._renderHiddenPanels()}
`;
}
private _renderHiddenPanels() { private _renderHiddenPanels() {
return html`${this._hiddenPanels.length return html`${this._hiddenPanels.length
? html`${this._hiddenPanels.map((url) => { ? html`${this._hiddenPanels.map((url) => {

View File

@ -316,6 +316,12 @@ class StepFlowCreateEntry extends LitElement {
overflow-y: auto; overflow-y: auto;
flex-direction: column; flex-direction: column;
} }
@media all and (max-width: 450px), all and (max-height: 500px) {
.devices {
/* header - margin content - footer */
max-height: calc(100vh - 52px - 20px - 52px);
}
}
.device { .device {
border: 1px solid var(--divider-color); border: 1px solid var(--divider-color);
padding: 6px; padding: 6px;
@ -352,11 +358,6 @@ class StepFlowCreateEntry extends LitElement {
margin-inline-start: auto; margin-inline-start: auto;
margin-inline-end: initial; margin-inline-end: initial;
} }
@media all and (max-width: 450px), all and (max-height: 500px) {
.device {
width: 100%;
}
}
.error { .error {
color: var(--error-color); color: var(--error-color);
} }

View File

@ -31,7 +31,7 @@ const DEFAULT_AGENTS = [];
class HaBackupConfigAgents extends LitElement { class HaBackupConfigAgents extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public cloudStatus!: CloudStatus; @property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ attribute: false }) public agents: BackupAgent[] = []; @property({ attribute: false }) public agents: BackupAgent[] = [];
@ -48,7 +48,10 @@ class HaBackupConfigAgents extends LitElement {
private _description(agentId: string) { private _description(agentId: string) {
if (agentId === CLOUD_AGENT) { if (agentId === CLOUD_AGENT) {
if (this.cloudStatus.logged_in && !this.cloudStatus.active_subscription) { if (
this.cloudStatus?.logged_in &&
!this.cloudStatus.active_subscription
) {
return this.hass.localize( return this.hass.localize(
"ui.panel.config.backup.agents.cloud_agent_no_subcription" "ui.panel.config.backup.agents.cloud_agent_no_subcription"
); );
@ -106,17 +109,17 @@ class HaBackupConfigAgents extends LitElement {
} }
private _availableAgents = memoizeOne( private _availableAgents = memoizeOne(
(agents: BackupAgent[], cloudStatus: CloudStatus) => (agents: BackupAgent[], cloudStatus?: CloudStatus) =>
agents.filter( agents.filter(
(agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus.logged_in (agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus?.logged_in
) )
); );
private _unavailableAgents = memoizeOne( private _unavailableAgents = memoizeOne(
( (
agents: BackupAgent[], agents: BackupAgent[],
cloudStatus: CloudStatus, selectedAgentIds: string[],
selectedAgentIds: string[] cloudStatus?: CloudStatus
) => { ) => {
const availableAgentIds = this._availableAgents(agents, cloudStatus).map( const availableAgentIds = this._availableAgents(agents, cloudStatus).map(
(agent) => agent.agent_id (agent) => agent.agent_id
@ -167,8 +170,8 @@ class HaBackupConfigAgents extends LitElement {
); );
const unavailableAgents = this._unavailableAgents( const unavailableAgents = this._unavailableAgents(
this.agents, this.agents,
this.cloudStatus, this._value,
this._value this.cloudStatus
); );
const allAgents = [...availableAgents, ...unavailableAgents]; const allAgents = [...availableAgents, ...unavailableAgents];
@ -187,7 +190,7 @@ class HaBackupConfigAgents extends LitElement {
const description = this._description(agentId); const description = this._description(agentId);
const noCloudSubscription = const noCloudSubscription =
agentId === CLOUD_AGENT && agentId === CLOUD_AGENT &&
this.cloudStatus.logged_in && this.cloudStatus?.logged_in &&
!this.cloudStatus.active_subscription; !this.cloudStatus.active_subscription;
return html` return html`

View File

@ -1,5 +1,5 @@
import { css, html, LitElement, nothing, type PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event"; import { fireEvent } from "../../../../../common/dom/fire_event";
import { clamp } from "../../../../../common/number/clamp"; import { clamp } from "../../../../../common/number/clamp";
import "../../../../../components/ha-expansion-panel"; import "../../../../../components/ha-expansion-panel";
@ -8,6 +8,7 @@ import "../../../../../components/ha-md-select";
import type { HaMdSelect } from "../../../../../components/ha-md-select"; import type { HaMdSelect } from "../../../../../components/ha-md-select";
import "../../../../../components/ha-md-select-option"; import "../../../../../components/ha-md-select-option";
import "../../../../../components/ha-md-textfield"; import "../../../../../components/ha-md-textfield";
import type { HaMdTextfield } from "../../../../../components/ha-md-textfield";
import type { BackupConfig, Retention } from "../../../../../data/backup"; import type { BackupConfig, Retention } from "../../../../../data/backup";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";
@ -54,16 +55,21 @@ class HaBackupConfigRetention extends LitElement {
@state() private _value = 3; @state() private _value = 3;
@query("#value") private _customValueField?: HaMdTextfield;
@query("#type") private _customTypeField?: HaMdSelect;
private _configLoaded = false;
private presetOptions = [ private presetOptions = [
RetentionPreset.COPIES_3, RetentionPreset.COPIES_3,
RetentionPreset.FOREVER, RetentionPreset.FOREVER,
RetentionPreset.CUSTOM, RetentionPreset.CUSTOM,
]; ];
public willUpdate(properties: PropertyValues) { public willUpdate() {
super.willUpdate(properties); if (!this._configLoaded && this.retention !== undefined) {
this._configLoaded = true;
if (!this.hasUpdated) {
if (!this.retention) { if (!this.retention) {
this._preset = RetentionPreset.GLOBAL; this._preset = RetentionPreset.GLOBAL;
} else if ( } else if (
@ -94,6 +100,10 @@ class HaBackupConfigRetention extends LitElement {
} }
protected render() { protected render() {
if (!this._configLoaded) {
return nothing;
}
return html` return html`
<ha-md-list-item> <ha-md-list-item>
<span slot="headline"> <span slot="headline">
@ -206,10 +216,12 @@ class HaBackupConfigRetention extends LitElement {
const clamped = clamp(value, MIN_VALUE, MAX_VALUE); const clamped = clamp(value, MIN_VALUE, MAX_VALUE);
target.value = clamped.toString(); target.value = clamped.toString();
const type = this._customTypeField?.value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
copies: this._type === "copies" ? clamped : null, copies: type === "copies" ? clamped : null,
days: this._type === "days" ? clamped : null, days: type === "days" ? clamped : null,
}, },
}); });
} }
@ -219,10 +231,12 @@ class HaBackupConfigRetention extends LitElement {
const target = ev.currentTarget as HaMdSelect; const target = ev.currentTarget as HaMdSelect;
const type = target.value as "copies" | "days"; const type = target.value as "copies" | "days";
const value = this._customValueField?.value;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
value: { value: {
copies: type === "copies" ? this._value : null, copies: type === "copies" ? Number(value) : null,
days: type === "days" ? this._value : null, days: type === "days" ? Number(value) : null,
}, },
}); });
} }

View File

@ -125,8 +125,10 @@ class HaConfigBackupDetails extends LitElement {
{ location: agentName } { location: agentName }
)} )}
.hass=${this.hass} .hass=${this.hass}
.retention=${this.config?.agents[this.agentId] .retention=${!this.config
?.retention} ? undefined
: this.config.agents[this.agentId]?.retention ||
null}
@value-changed=${this._retentionChanged} @value-changed=${this._retentionChanged}
></ha-backup-config-retention>`} ></ha-backup-config-retention>`}
</ha-card> </ha-card>

View File

@ -41,7 +41,7 @@ import { brandsUrl } from "../../../util/brands-url";
class HaConfigBackupSettings extends LitElement { class HaConfigBackupSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public cloudStatus!: CloudStatus; @property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ type: Boolean }) public narrow = false; @property({ type: Boolean }) public narrow = false;
@ -244,7 +244,7 @@ class HaConfigBackupSettings extends LitElement {
` `
: nothing} : nothing}
</div> </div>
${!this.cloudStatus.logged_in ${!this.cloudStatus?.logged_in
? html`<ha-card class="cloud-info"> ? html`<ha-card class="cloud-info">
<div class="cloud-header"> <div class="cloud-header">
<img <img

View File

@ -29,9 +29,8 @@ class DialogBluetoothDeviceInfo extends LitElement implements HassDialog {
} }
public showDataAsHex(bytestring: string): string { public showDataAsHex(bytestring: string): string {
return Array.from(new TextEncoder().encode(bytestring)) const bytes = bytestring.match(/.{2}/g) ?? [];
.map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")) return bytes.map((byte) => `0x${byte.toUpperCase()}`).join(" ");
.join(" ");
} }
private async _copyToClipboard(): Promise<void> { private async _copyToClipboard(): Promise<void> {

View File

@ -9,6 +9,7 @@ import type { SSDPDiscoveryInfoDialogParams } from "./show-dialog-ssdp-discovery
import "../../../../../components/ha-button"; import "../../../../../components/ha-button";
import { showToast } from "../../../../../util/toast"; import { showToast } from "../../../../../util/toast";
import { copyToClipboard } from "../../../../../common/util/copy-clipboard"; import { copyToClipboard } from "../../../../../common/util/copy-clipboard";
import { showSSDPRawDataDialog } from "./show-dialog-ssdp-raw-data";
@customElement("dialog-ssdp-device-info") @customElement("dialog-ssdp-device-info")
class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog { class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
@ -39,6 +40,16 @@ class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
}); });
} }
private _showRawData(key: string, data: Record<string, unknown>) {
return (e: Event) => {
e.preventDefault();
showSSDPRawDataDialog(this, {
key,
data,
});
};
}
protected render(): TemplateResult | typeof nothing { protected render(): TemplateResult | typeof nothing {
if (!this._params) { if (!this._params) {
return nothing; return nothing;
@ -83,7 +94,20 @@ class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog {
([key, value]) => html` ([key, value]) => html`
<tr> <tr>
<td><b>${key}</b></td> <td><b>${key}</b></td>
<td>${value}</td> <td>
${typeof value === "object" && value !== null
? html`<a
href="#"
@click=${this._showRawData(
key,
value as Record<string, unknown>
)}
>${this.hass.localize(
"ui.panel.config.ssdp.show_raw_data"
)}</a
>`
: value}
</td>
</tr> </tr>
` `
)} )}

View File

@ -0,0 +1,68 @@
import { LitElement, html, nothing, css } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { TemplateResult } from "lit";
import { dump } from "js-yaml";
import { fireEvent } from "../../../../../common/dom/fire_event";
import type { HassDialog } from "../../../../../dialogs/make-dialog-manager";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import type { HomeAssistant } from "../../../../../types";
import "../../../../../components/ha-code-editor";
export interface SSDPRawDataDialogParams {
key: string;
data: Record<string, unknown>;
}
@customElement("dialog-ssdp-raw-data")
class DialogSSDPRawData extends LitElement implements HassDialog {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: SSDPRawDataDialogParams;
public async showDialog(params: SSDPRawDataDialogParams): Promise<void> {
this._params = params;
}
public closeDialog(): boolean {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
return true;
}
protected render(): TemplateResult | typeof nothing {
if (!this._params) {
return nothing;
}
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
`${this.hass.localize("ui.panel.config.ssdp.raw_data_title")}: ${this._params.key}`
)}
>
<ha-code-editor
mode="yaml"
.value=${dump(this._params.data)}
readonly
autofocus
></ha-code-editor>
</ha-dialog>
`;
}
static styles = css`
ha-code-editor {
--code-mirror-max-height: 60vh;
--code-mirror-height: auto;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"dialog-ssdp-raw-data": DialogSSDPRawData;
}
}

View File

@ -0,0 +1,19 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface SSDPRawDataDialogParams {
key: string;
data: Record<string, unknown>;
}
export const loadSSDPRawDataDialog = () => import("./dialog-ssdp-raw-data");
export const showSSDPRawDataDialog = (
element: HTMLElement,
ssdpRawDataDialogParams: SSDPRawDataDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-ssdp-raw-data",
dialogImport: loadSSDPRawDataDialog,
dialogParams: ssdpRawDataDialogParams,
});
};

View File

@ -1,7 +1,7 @@
import { css } from "lit"; import { css } from "lit";
export const sidebarEditStyle = css` export const sidebarEditStyle = css`
ha-sortable ha-md-list-item:nth-child(2n) { ha-sortable ha-md-list-item.draggable:nth-child(2n) {
animation-name: keyframes1; animation-name: keyframes1;
animation-iteration-count: infinite; animation-iteration-count: infinite;
transform-origin: 50% 10%; transform-origin: 50% 10%;
@ -9,7 +9,7 @@ export const sidebarEditStyle = css`
animation-duration: 0.25s; animation-duration: 0.25s;
} }
ha-sortable ha-md-list-item:nth-child(2n-1) { ha-sortable ha-md-list-item.draggable:nth-child(2n-1) {
animation-name: keyframes2; animation-name: keyframes2;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-direction: alternate; animation-direction: alternate;
@ -18,8 +18,7 @@ export const sidebarEditStyle = css`
animation-duration: 0.33s; animation-duration: 0.33s;
} }
ha-sortable ha-md-list-item { ha-sortable ha-md-list-item.draggable {
height: 48px;
cursor: grab; cursor: grab;
} }

View File

@ -5586,7 +5586,9 @@
"upnp": "Universal Plug and Play (UPnP)", "upnp": "Universal Plug and Play (UPnP)",
"discovery_information": "Discovery information", "discovery_information": "Discovery information",
"copy_to_clipboard": "Copy to clipboard", "copy_to_clipboard": "Copy to clipboard",
"no_devices_found": "No matching SSDP/UPnP discoveries found" "no_devices_found": "No matching SSDP/UPnP discoveries found",
"show_raw_data": "Show raw data",
"raw_data_title": "Raw data"
}, },
"zeroconf": { "zeroconf": {
"name": "Name", "name": "Name",