mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-24 18:27:19 +00:00
Compare commits
3 Commits
device-inf
...
encryption
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52ffc9c1e4 | ||
|
|
cbed509349 | ||
|
|
197eaa32a0 |
14
src/data/esphome.ts
Normal file
14
src/data/esphome.ts
Normal 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,
|
||||
});
|
||||
@@ -1,9 +1,7 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { titleCase } from "../../../../common/string/title-case";
|
||||
import "../../../../components/ha-card";
|
||||
import type { DeviceRegistryEntry } from "../../../../data/device_registry";
|
||||
@@ -11,61 +9,16 @@ import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { createSearchParam } from "../../../../common/url/search-params";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-label";
|
||||
import type { LabelRegistryEntry } from "../../../../data/label_registry";
|
||||
import { subscribeLabelRegistry } from "../../../../data/label_registry";
|
||||
import { computeCssColor } from "../../../../common/color/compute-color";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
|
||||
@customElement("ha-device-info-card")
|
||||
export class HaDeviceCard extends SubscribeMixin(LitElement) {
|
||||
export class HaDeviceCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _labelRegistry?: LabelRegistryEntry[];
|
||||
|
||||
private _labelsData = memoizeOne(
|
||||
(
|
||||
labels: LabelRegistryEntry[] | undefined,
|
||||
labelIds: string[],
|
||||
language: string
|
||||
): {
|
||||
map: Map<string, LabelRegistryEntry>;
|
||||
ids: string[];
|
||||
} => {
|
||||
const map = labels
|
||||
? new Map(labels.map((label) => [label.label_id, label]))
|
||||
: new Map<string, LabelRegistryEntry>();
|
||||
const ids = [...labelIds].sort((labelA, labelB) =>
|
||||
stringCompare(
|
||||
map.get(labelA)?.name || labelA,
|
||||
map.get(labelB)?.name || labelB,
|
||||
language
|
||||
)
|
||||
);
|
||||
return { map, ids };
|
||||
}
|
||||
);
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||
this._labelRegistry = labels;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { map: labelMap, ids: labels } = this._labelsData(
|
||||
this._labelRegistry,
|
||||
this.device.labels,
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
@@ -105,7 +58,7 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
|
||||
<span class="hub"
|
||||
><a
|
||||
href="/config/devices/device/${this.device.via_device_id}"
|
||||
>${this._computeDeviceNameDisplay(
|
||||
>${this._computeDeviceNameDislay(
|
||||
this.device.via_device_id
|
||||
)}</a
|
||||
></span
|
||||
@@ -173,34 +126,6 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${labels.length > 0
|
||||
? html`
|
||||
<div class="extra-info labels">
|
||||
${labels.map((labelId) => {
|
||||
const label = labelMap.get(labelId);
|
||||
const color =
|
||||
label?.color && typeof label.color === "string"
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label?.description}
|
||||
>
|
||||
${label?.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${label.icon}
|
||||
></ha-icon>`
|
||||
: nothing}
|
||||
${label?.name || labelId}
|
||||
</ha-label>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="actions"></slot>
|
||||
@@ -214,7 +139,7 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
|
||||
);
|
||||
}
|
||||
|
||||
private _computeDeviceNameDisplay(deviceId: string) {
|
||||
private _computeDeviceNameDislay(deviceId) {
|
||||
const device = this.hass.devices[deviceId];
|
||||
return device
|
||||
? computeDeviceNameDisplay(device, this.hass)
|
||||
@@ -237,26 +162,8 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
|
||||
.device {
|
||||
width: 30%;
|
||||
}
|
||||
.labels {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-1);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.labels ha-label {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
--ha-label-text-color: var(--primary-text-color);
|
||||
--ha-label-icon-color: var(--primary-text-color);
|
||||
}
|
||||
.extra-info {
|
||||
margin-top: var(--ha-space-2);
|
||||
margin-top: 8px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.manuf,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
};
|
||||
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user