diff --git a/pyproject.toml b/pyproject.toml index 5d0717ac3c..2eec5c49cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20250507.0" +version = "20250509.0" license = "Apache-2.0" license-files = ["LICENSE*"] description = "The Home Assistant frontend" diff --git a/src/components/ha-dialog.ts b/src/components/ha-dialog.ts index e326e6b19b..817bfe92ac 100644 --- a/src/components/ha-dialog.ts +++ b/src/components/ha-dialog.ts @@ -90,7 +90,7 @@ export class HaDialog extends DialogBase { } .mdc-dialog__actions { 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) { flex: var(--secondary-action-button-flex, unset); @@ -107,9 +107,6 @@ export class HaDialog extends DialogBase { .mdc-dialog__title:has(span) { padding: 12px 12px 0; } - .mdc-dialog__actions { - padding: 12px 24px 12px 24px; - } .mdc-dialog__title::before { content: unset; } diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts index 2c3965f56a..82f7ae547e 100644 --- a/src/components/ha-service-control.ts +++ b/src/components/ha-service-control.ts @@ -83,7 +83,7 @@ export class HaServiceControl extends LitElement { @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 = false; @@ -895,6 +895,9 @@ export class HaServiceControl extends LitElement { ha-settings-row { padding: var(--service-control-padding, 0 16px); } + ha-settings-row[narrow] { + padding-bottom: 8px; + } ha-settings-row { --settings-row-content-width: 100%; --settings-row-prefix-display: contents; @@ -916,7 +919,7 @@ export class HaServiceControl extends LitElement { margin: var(--service-control-padding, 0 16px); padding: 16px 0; } - :host([hidePicker]) p { + :host([hide-picker]) p { padding-top: 0; } .checkbox-spacer { diff --git a/src/components/ha-sidebar.ts b/src/components/ha-sidebar.ts index a286613cb0..915515ffea 100644 --- a/src/components/ha-sidebar.ts +++ b/src/components/ha-sidebar.ts @@ -24,9 +24,10 @@ import { customElement, eventOptions, property, - state, query, + state, } from "lit/decorators"; +import { classMap } from "lit/directives/class-map"; import memoizeOne from "memoize-one"; import { storage } from "../common/decorators/storage"; import { fireEvent } from "../common/dom/fire_event"; @@ -45,13 +46,13 @@ import { haStyleScrollbar } from "../resources/styles"; import type { HomeAssistant, PanelInfo, Route } from "../types"; import "./ha-icon"; 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-sortable"; import "./ha-svg-icon"; 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"]; @@ -407,6 +408,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { // prettier-ignore return html` + + + `; } - private _renderPanels(panels: PanelInfo[], selectedPanel: string) { + private _renderPanels( + panels: PanelInfo[], + selectedPanel: string, + sortable = false + ) { return panels.map((panel) => this._renderPanel( panel.url_path, @@ -437,17 +444,26 @@ class HaSidebar extends SubscribeMixin(LitElement) { : panel.url_path in PANEL_ICONS ? PANEL_ICONS[panel.url_path] : undefined, - selectedPanel + selectedPanel, + sortable ) ); } + private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) { + return html` + ${this._renderPanels(beforeSpacer, selectedPanel, true)} + ${this._renderSpacer()}${this._renderHiddenPanels()} + `; + } + private _renderPanel( urlPath: string, title: string | null, icon: string | null | undefined, iconPath: string | null | undefined, - selectedPanel: string + selectedPanel: string, + sortable = false ) { return urlPath === "config" ? this._renderConfiguration(title, selectedPanel) @@ -455,7 +471,10 @@ class HaSidebar extends SubscribeMixin(LitElement) { @@ -496,15 +515,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { this._panelOrder = panelOrder; } - private _renderPanelsEdit(beforeSpacer: PanelInfo[], selectedPanel: string) { - return html` -
${this._renderPanels(beforeSpacer, selectedPanel)}
-
- ${this._renderSpacer()}${this._renderHiddenPanels()} - `; - } - private _renderHiddenPanels() { return html`${this._hiddenPanels.length ? html`${this._hiddenPanels.map((url) => { diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 327b5b783a..eac44ccff9 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -316,6 +316,12 @@ class StepFlowCreateEntry extends LitElement { overflow-y: auto; 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 { border: 1px solid var(--divider-color); padding: 6px; @@ -352,11 +358,6 @@ class StepFlowCreateEntry extends LitElement { margin-inline-start: auto; margin-inline-end: initial; } - @media all and (max-width: 450px), all and (max-height: 500px) { - .device { - width: 100%; - } - } .error { color: var(--error-color); } diff --git a/src/panels/config/backup/components/config/ha-backup-config-agents.ts b/src/panels/config/backup/components/config/ha-backup-config-agents.ts index f808d3fe99..686d6bd0a5 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-agents.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-agents.ts @@ -31,7 +31,7 @@ const DEFAULT_AGENTS = []; class HaBackupConfigAgents extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public cloudStatus!: CloudStatus; + @property({ attribute: false }) public cloudStatus?: CloudStatus; @property({ attribute: false }) public agents: BackupAgent[] = []; @@ -48,7 +48,10 @@ class HaBackupConfigAgents extends LitElement { private _description(agentId: string) { 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( "ui.panel.config.backup.agents.cloud_agent_no_subcription" ); @@ -106,17 +109,17 @@ class HaBackupConfigAgents extends LitElement { } private _availableAgents = memoizeOne( - (agents: BackupAgent[], cloudStatus: CloudStatus) => + (agents: BackupAgent[], cloudStatus?: CloudStatus) => agents.filter( - (agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus.logged_in + (agent) => agent.agent_id !== CLOUD_AGENT || cloudStatus?.logged_in ) ); private _unavailableAgents = memoizeOne( ( agents: BackupAgent[], - cloudStatus: CloudStatus, - selectedAgentIds: string[] + selectedAgentIds: string[], + cloudStatus?: CloudStatus ) => { const availableAgentIds = this._availableAgents(agents, cloudStatus).map( (agent) => agent.agent_id @@ -167,8 +170,8 @@ class HaBackupConfigAgents extends LitElement { ); const unavailableAgents = this._unavailableAgents( this.agents, - this.cloudStatus, - this._value + this._value, + this.cloudStatus ); const allAgents = [...availableAgents, ...unavailableAgents]; @@ -187,7 +190,7 @@ class HaBackupConfigAgents extends LitElement { const description = this._description(agentId); const noCloudSubscription = agentId === CLOUD_AGENT && - this.cloudStatus.logged_in && + this.cloudStatus?.logged_in && !this.cloudStatus.active_subscription; return html` diff --git a/src/panels/config/backup/components/config/ha-backup-config-retention.ts b/src/panels/config/backup/components/config/ha-backup-config-retention.ts index 245f700600..05b0564eb0 100644 --- a/src/panels/config/backup/components/config/ha-backup-config-retention.ts +++ b/src/panels/config/backup/components/config/ha-backup-config-retention.ts @@ -1,5 +1,5 @@ -import { css, html, LitElement, nothing, type PropertyValues } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property, query, state } from "lit/decorators"; import { fireEvent } from "../../../../../common/dom/fire_event"; import { clamp } from "../../../../../common/number/clamp"; import "../../../../../components/ha-expansion-panel"; @@ -8,6 +8,7 @@ import "../../../../../components/ha-md-select"; import type { HaMdSelect } from "../../../../../components/ha-md-select"; import "../../../../../components/ha-md-select-option"; import "../../../../../components/ha-md-textfield"; +import type { HaMdTextfield } from "../../../../../components/ha-md-textfield"; import type { BackupConfig, Retention } from "../../../../../data/backup"; import type { HomeAssistant } from "../../../../../types"; @@ -54,16 +55,21 @@ class HaBackupConfigRetention extends LitElement { @state() private _value = 3; + @query("#value") private _customValueField?: HaMdTextfield; + + @query("#type") private _customTypeField?: HaMdSelect; + + private _configLoaded = false; + private presetOptions = [ RetentionPreset.COPIES_3, RetentionPreset.FOREVER, RetentionPreset.CUSTOM, ]; - public willUpdate(properties: PropertyValues) { - super.willUpdate(properties); - - if (!this.hasUpdated) { + public willUpdate() { + if (!this._configLoaded && this.retention !== undefined) { + this._configLoaded = true; if (!this.retention) { this._preset = RetentionPreset.GLOBAL; } else if ( @@ -94,6 +100,10 @@ class HaBackupConfigRetention extends LitElement { } protected render() { + if (!this._configLoaded) { + return nothing; + } + return html` @@ -206,10 +216,12 @@ class HaBackupConfigRetention extends LitElement { const clamped = clamp(value, MIN_VALUE, MAX_VALUE); target.value = clamped.toString(); + const type = this._customTypeField?.value; + fireEvent(this, "value-changed", { value: { - copies: this._type === "copies" ? clamped : null, - days: this._type === "days" ? clamped : null, + copies: type === "copies" ? clamped : null, + days: type === "days" ? clamped : null, }, }); } @@ -219,10 +231,12 @@ class HaBackupConfigRetention extends LitElement { const target = ev.currentTarget as HaMdSelect; const type = target.value as "copies" | "days"; + const value = this._customValueField?.value; + fireEvent(this, "value-changed", { value: { - copies: type === "copies" ? this._value : null, - days: type === "days" ? this._value : null, + copies: type === "copies" ? Number(value) : null, + days: type === "days" ? Number(value) : null, }, }); } diff --git a/src/panels/config/backup/ha-config-backup-location.ts b/src/panels/config/backup/ha-config-backup-location.ts index 323ca5097c..802f5e03dc 100644 --- a/src/panels/config/backup/ha-config-backup-location.ts +++ b/src/panels/config/backup/ha-config-backup-location.ts @@ -125,8 +125,10 @@ class HaConfigBackupDetails extends LitElement { { location: agentName } )} .hass=${this.hass} - .retention=${this.config?.agents[this.agentId] - ?.retention} + .retention=${!this.config + ? undefined + : this.config.agents[this.agentId]?.retention || + null} @value-changed=${this._retentionChanged} >`} diff --git a/src/panels/config/backup/ha-config-backup-settings.ts b/src/panels/config/backup/ha-config-backup-settings.ts index 422f96d69c..c7e0bf0f28 100644 --- a/src/panels/config/backup/ha-config-backup-settings.ts +++ b/src/panels/config/backup/ha-config-backup-settings.ts @@ -41,7 +41,7 @@ import { brandsUrl } from "../../../util/brands-url"; class HaConfigBackupSettings extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property({ attribute: false }) public cloudStatus!: CloudStatus; + @property({ attribute: false }) public cloudStatus?: CloudStatus; @property({ type: Boolean }) public narrow = false; @@ -244,7 +244,7 @@ class HaConfigBackupSettings extends LitElement { ` : nothing} - ${!this.cloudStatus.logged_in + ${!this.cloudStatus?.logged_in ? html`
${supervisor - ? html` + ? html`
${this.hass.localize( "ui.panel.config.backup.settings.addon_update_backup.title" diff --git a/src/panels/config/integrations/integration-panels/bluetooth/dialog-bluetooth-device-info.ts b/src/panels/config/integrations/integration-panels/bluetooth/dialog-bluetooth-device-info.ts index 25c09a9284..71ee7224c2 100644 --- a/src/panels/config/integrations/integration-panels/bluetooth/dialog-bluetooth-device-info.ts +++ b/src/panels/config/integrations/integration-panels/bluetooth/dialog-bluetooth-device-info.ts @@ -29,9 +29,8 @@ class DialogBluetoothDeviceInfo extends LitElement implements HassDialog { } public showDataAsHex(bytestring: string): string { - return Array.from(new TextEncoder().encode(bytestring)) - .map((byte) => byte.toString(16).toUpperCase().padStart(2, "0")) - .join(" "); + const bytes = bytestring.match(/.{2}/g) ?? []; + return bytes.map((byte) => `0x${byte.toUpperCase()}`).join(" "); } private async _copyToClipboard(): Promise { diff --git a/src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-discovery-info.ts b/src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-discovery-info.ts index f349b358a6..bfe1031932 100644 --- a/src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-discovery-info.ts +++ b/src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-discovery-info.ts @@ -9,6 +9,7 @@ import type { SSDPDiscoveryInfoDialogParams } from "./show-dialog-ssdp-discovery import "../../../../../components/ha-button"; import { showToast } from "../../../../../util/toast"; import { copyToClipboard } from "../../../../../common/util/copy-clipboard"; +import { showSSDPRawDataDialog } from "./show-dialog-ssdp-raw-data"; @customElement("dialog-ssdp-device-info") class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog { @@ -39,6 +40,16 @@ class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog { }); } + private _showRawData(key: string, data: Record) { + return (e: Event) => { + e.preventDefault(); + showSSDPRawDataDialog(this, { + key, + data, + }); + }; + } + protected render(): TemplateResult | typeof nothing { if (!this._params) { return nothing; @@ -83,7 +94,20 @@ class DialogSSDPDiscoveryInfo extends LitElement implements HassDialog { ([key, value]) => html` ${key} - ${value} + + ${typeof value === "object" && value !== null + ? html` + )} + >${this.hass.localize( + "ui.panel.config.ssdp.show_raw_data" + )}` + : value} + ` )} diff --git a/src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-raw-data.ts b/src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-raw-data.ts new file mode 100644 index 0000000000..6dade384c3 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ssdp/dialog-ssdp-raw-data.ts @@ -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; +} + +@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 { + 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` + + + + `; + } + + static styles = css` + ha-code-editor { + --code-mirror-max-height: 60vh; + --code-mirror-height: auto; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-ssdp-raw-data": DialogSSDPRawData; + } +} diff --git a/src/panels/config/integrations/integration-panels/ssdp/show-dialog-ssdp-raw-data.ts b/src/panels/config/integrations/integration-panels/ssdp/show-dialog-ssdp-raw-data.ts new file mode 100644 index 0000000000..32336a68fc --- /dev/null +++ b/src/panels/config/integrations/integration-panels/ssdp/show-dialog-ssdp-raw-data.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export interface SSDPRawDataDialogParams { + key: string; + data: Record; +} + +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, + }); +}; diff --git a/src/resources/ha-sidebar-edit-style.ts b/src/resources/ha-sidebar-edit-style.ts index efd4ddee10..fa09d6fb8a 100644 --- a/src/resources/ha-sidebar-edit-style.ts +++ b/src/resources/ha-sidebar-edit-style.ts @@ -1,7 +1,7 @@ import { css } from "lit"; 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-iteration-count: infinite; transform-origin: 50% 10%; @@ -9,7 +9,7 @@ export const sidebarEditStyle = css` 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-iteration-count: infinite; animation-direction: alternate; @@ -18,8 +18,7 @@ export const sidebarEditStyle = css` animation-duration: 0.33s; } - ha-sortable ha-md-list-item { - height: 48px; + ha-sortable ha-md-list-item.draggable { cursor: grab; } diff --git a/src/translations/en.json b/src/translations/en.json index 6d9e99d474..11f047653f 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -5586,7 +5586,9 @@ "upnp": "Universal Plug and Play (UPnP)", "discovery_information": "Discovery information", "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": { "name": "Name",