Compare commits

...

4 Commits

Author SHA1 Message Date
stvncode
52ffc9c1e4 Reviews 2025-11-24 16:22:45 +01:00
Steven Travers
cbed509349 Merge branch 'dev' into encryption-key 2025-11-24 13:39:31 +01:00
stvncode
197eaa32a0 Show encryption key in actions for esphome device 2025-11-24 13:09:05 +01:00
Wendelin
6890823c31 Make login button full width (#28072) 2025-11-24 10:49:13 +01:00
7 changed files with 241 additions and 1 deletions

View File

@@ -2,8 +2,8 @@
import { genClientId } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { keyed } from "lit/directives/keyed";
import { customElement, property, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import type { LocalizeFunc } from "../common/translations/localize";
import "../components/ha-alert";
import "../components/ha-button";
@@ -118,6 +118,9 @@ export class HaAuthFlow extends LitElement {
display: block;
margin-top: 16px;
}
.action ha-button {
width: 100%;
}
</style>
<form>${this._renderForm()}</form>
`;

14
src/data/esphome.ts Normal file
View File

@@ -0,0 +1,14 @@
import type { HomeAssistant } from "../types";
export interface ESPHomeEncryptionKey {
encryption_key: string;
}
export const fetchESPHomeEncryptionKey = (
hass: HomeAssistant,
entry_id: string
): Promise<ESPHomeEncryptionKey> =>
hass.callWS({
type: "esphome/get_encryption_key",
entry_id,
});

View File

@@ -0,0 +1,52 @@
import { mdiKey } from "@mdi/js";
import { getConfigEntries } from "../../../../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchESPHomeEncryptionKey } from "../../../../../../data/esphome";
import type { HomeAssistant } from "../../../../../../types";
import { showESPHomeEncryptionKeyDialog } from "../../../../integrations/integration-panels/esphome/show-dialog-esphome-encryption-key";
import type { DeviceAction } from "../../../ha-config-device-page";
export const getESPHomeDeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
const actions: DeviceAction[] = [];
const configEntries = await getConfigEntries(hass, {
domain: "esphome",
});
const configEntry = configEntries.find((entry) =>
device.config_entries.includes(entry.entry_id)
);
if (!configEntry) {
return [];
}
const entryId = configEntry.entry_id;
try {
const encryptionKey = await fetchESPHomeEncryptionKey(hass, entryId);
if (encryptionKey.encryption_key) {
actions.push({
label: hass.localize(
"ui.panel.config.devices.esphome.show_encryption_key"
),
icon: mdiKey,
action: () =>
showESPHomeEncryptionKeyDialog(el, {
entry_id: entryId,
encryption_key: encryptionKey.encryption_key,
}),
});
}
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to fetch ESPHome encryption key:", err);
}
return actions;
};

View File

@@ -1162,6 +1162,17 @@ export class HaConfigDevicePage extends LitElement {
);
deviceActions.push(...actions);
}
if (domains.includes("esphome")) {
const esphome = await import(
"./device-detail/integration-elements/esphome/device-actions"
);
const actions = await esphome.getESPHomeDeviceActions(
this,
this.hass,
device
);
deviceActions.push(...actions);
}
if (domains.includes("matter")) {
const matter = await import(
"./device-detail/integration-elements/matter/device-actions"

View File

@@ -0,0 +1,135 @@
import { mdiClose, mdiContentCopy } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../../common/util/copy-clipboard";
import "../../../../../components/ha-dialog-header";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-wa-dialog";
import { haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { showToast } from "../../../../../util/toast";
import type { ESPHomeEncryptionKeyDialogParams } from "./show-dialog-esphome-encryption-key";
@customElement("dialog-esphome-encryption-key")
class DialogESPHomeEncryptionKey extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: ESPHomeEncryptionKeyDialogParams;
public async showDialog(
params: ESPHomeEncryptionKeyDialogParams
): Promise<void> {
this._params = params;
}
public closeDialog(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-wa-dialog
open
@closed=${this.closeDialog}
hideActions
header-title=${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_title"
)}
>
<ha-dialog-header slot="heading">
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">
${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_title"
)}
</span>
</ha-dialog-header>
<div class="content">
<p>
${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_description"
)}
</p>
<div class="key-row">
<div class="key-container">
<code>${this._params.encryption_key}</code>
</div>
<ha-icon-button
@click=${this._copyToClipboard}
.label=${this.hass.localize("ui.common.copy")}
.path=${mdiContentCopy}
></ha-icon-button>
</div>
</div>
</ha-wa-dialog>
`;
}
private async _copyToClipboard(): Promise<void> {
if (!this._params?.encryption_key) {
return;
}
await copyToClipboard(this._params.encryption_key);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
.content {
display: flex;
flex-direction: column;
gap: var(--ha-space-6);
}
.key-row {
display: flex;
gap: var(--ha-space-2);
align-items: center;
}
.key-container {
flex: 1;
border-radius: var(--ha-space-2);
border: 1px solid var(--divider-color);
background-color: var(--code-editor-background-color, #f8f8f8);
padding: var(--ha-space-3);
overflow: hidden;
}
p {
margin: 0;
color: var(--secondary-text-color);
line-height: var(--ha-line-height-condensed);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-esphome-encryption-key": DialogESPHomeEncryptionKey;
}
}

View File

@@ -0,0 +1,20 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ESPHomeEncryptionKeyDialogParams {
entry_id: string;
encryption_key: string;
}
export const loadESPHomeEncryptionKeyDialog = () =>
import("./dialog-esphome-encryption-key");
export const showESPHomeEncryptionKeyDialog = (
element: HTMLElement,
dialogParams: ESPHomeEncryptionKeyDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-esphome-encryption-key",
dialogImport: loadESPHomeEncryptionKeyDialog,
dialogParams,
});
};

View File

@@ -5412,6 +5412,11 @@
"partial_failure": "Some devices failed to delete successfully. Check system logs for more information."
}
}
},
"esphome": {
"show_encryption_key": "Show encryption key",
"encryption_key_title": "ESPHome Encryption Key",
"encryption_key_description": "This is the encryption key for your ESPHome device. Keep it in a safe place, as you may need it when transferring devices between Home Assistant instances."
}
},
"entities": {