Compare commits

..

2 Commits

Author SHA1 Message Date
Aidan Timson
11697f6627 Use default 2025-11-24 15:05:22 +00:00
Aidan Timson
648a4888a3 Use entity naming for helper names 2025-11-24 15:01:20 +00:00
24 changed files with 129 additions and 1103 deletions

View File

@@ -112,7 +112,7 @@
"google-timezones-json": "1.2.0",
"gulp-zopfli-green": "6.0.2",
"hls.js": "1.6.14",
"home-assistant-js-websocket": "9.6.0",
"home-assistant-js-websocket": "9.5.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "10.7.18",
"js-yaml": "4.1.1",

View File

@@ -75,15 +75,11 @@ export class HaDialogHeader extends LitElement {
font-size: var(--ha-font-size-xl);
line-height: var(--ha-line-height-condensed);
font-weight: var(--ha-font-weight-medium);
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
}
.header-subtitle {
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-normal);
color: var(
--ha-dialog-header-subtitle-color,
var(--secondary-text-color)
);
color: var(--secondary-text-color);
}
@media all and (min-width: 450px) and (min-height: 500px) {
.header-bar {

View File

@@ -209,7 +209,6 @@ export class HaExpansionPanel extends LitElement {
::slotted([slot="header"]) {
flex: 1;
overflow-wrap: anywhere;
color: var(--primary-text-color);
}
.container {

View File

@@ -1,178 +0,0 @@
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { HomeAssistant } from "../types";
import { subscribeLabFeatures } from "../data/labs";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
interface Snowflake {
id: number;
left: number;
size: number;
duration: number;
delay: number;
blur: number;
}
@customElement("ha-snowflakes")
export class HaSnowflakes extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ type: Boolean }) public narrow = false;
@state() private _enabled = false;
@state() private _snowflakes: Snowflake[] = [];
private _maxSnowflakes = 50;
public hassSubscribe() {
return [
subscribeLabFeatures(this.hass!.connection, (features) => {
this._enabled =
features.find(
(f) =>
f.domain === "frontend" && f.preview_feature === "winter_mode"
)?.enabled ?? false;
}),
];
}
private _generateSnowflakes() {
if (!this._enabled) {
this._snowflakes = [];
return;
}
const snowflakes: Snowflake[] = [];
for (let i = 0; i < this._maxSnowflakes; i++) {
snowflakes.push({
id: i,
left: Math.random() * 100, // Random position from 0-100%
size: Math.random() * 12 + 8, // Random size between 8-20px
duration: Math.random() * 8 + 8, // Random duration between 8-16s
delay: Math.random() * 8, // Random delay between 0-8s
blur: Math.random() * 1, // Random blur between 0-1px
});
}
this._snowflakes = snowflakes;
}
protected willUpdate(changedProps: Map<string, unknown>) {
super.willUpdate(changedProps);
if (changedProps.has("_enabled")) {
this._generateSnowflakes();
}
}
protected render() {
if (!this._enabled) {
return nothing;
}
const isDark = this.hass?.themes.darkMode ?? false;
return html`
<div class="snowflakes ${isDark ? "dark" : "light"}" aria-hidden="true">
${this._snowflakes.map(
(flake) => html`
<div
class="snowflake ${this.narrow && flake.id >= 30
? "hide-narrow"
: ""}"
style="
left: ${flake.left}%;
font-size: ${flake.size}px;
animation-duration: ${flake.duration}s;
animation-delay: ${flake.delay}s;
filter: blur(${flake.blur}px);
"
>
</div>
`
)}
</div>
`;
}
static readonly styles = css`
:host {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 9999;
overflow: hidden;
}
.snowflakes {
position: absolute;
top: -10%;
left: 0;
width: 100%;
height: 110%;
pointer-events: none;
}
.snowflake {
position: absolute;
top: -10%;
opacity: 0.7;
user-select: none;
pointer-events: none;
animation: fall linear infinite;
}
.light .snowflake {
color: #00bcd4;
text-shadow:
0 0 5px #00bcd4,
0 0 10px #00e5ff;
}
.dark .snowflake {
color: #fff;
text-shadow:
0 0 5px rgba(255, 255, 255, 0.8),
0 0 10px rgba(255, 255, 255, 0.5);
}
.snowflake.hide-narrow {
display: none;
}
@keyframes fall {
0% {
transform: translateY(-10vh) translateX(0);
}
25% {
transform: translateY(30vh) translateX(10px);
}
50% {
transform: translateY(60vh) translateX(-10px);
}
75% {
transform: translateY(85vh) translateX(10px);
}
100% {
transform: translateY(120vh) translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.snowflake {
animation: none;
display: none;
}
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-snowflakes": HaSnowflakes;
}
}

View File

@@ -5,7 +5,6 @@ export interface AnalyticsPreferences {
diagnostics?: boolean;
usage?: boolean;
statistics?: boolean;
snapshots?: boolean;
}
export interface Analytics {

View File

@@ -214,8 +214,6 @@ export interface PipelineRun {
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
run: PipelineRunStartEvent["data"];
error?: PipelineErrorEvent["data"];
started: Date;
finished?: Date;
wake_word?: PipelineWakeWordStartEvent["data"] &
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
stt?: PipelineSTTStartEvent["data"] &
@@ -237,7 +235,6 @@ export const processEvent = (
stage: "ready",
run: event.data,
events: [event],
started: new Date(event.timestamp),
};
return run;
}
@@ -293,14 +290,9 @@ export const processEvent = (
tts: { ...run.tts!, ...event.data, done: true },
};
} else if (event.type === "run-end") {
run = { ...run, finished: new Date(event.timestamp), stage: "done" };
run = { ...run, stage: "done" };
} else if (event.type === "error") {
run = {
...run,
finished: new Date(event.timestamp),
stage: "error",
error: event.data,
};
run = { ...run, stage: "error", error: event.data };
} else {
run = { ...run };
}

View File

@@ -1,228 +0,0 @@
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
export const enum ChatLogEventType {
INITIAL_STATE = "initial_state",
CREATED = "created",
UPDATED = "updated",
DELETED = "deleted",
CONTENT_ADDED = "content_added",
}
export interface ChatLogAttachment {
media_content_id: string;
mime_type: string;
path: string;
}
export interface ChatLogSystemContent {
role: "system";
content: string;
created: Date;
}
export interface ChatLogUserContent {
role: "user";
content: string;
created: Date;
attachments?: ChatLogAttachment[];
}
export interface ChatLogAssistantContent {
role: "assistant";
agent_id: string;
created: Date;
content?: string;
thinking_content?: string;
tool_calls?: any[];
}
export interface ChatLogToolResultContent {
role: "tool_result";
agent_id: string;
tool_call_id: string;
tool_name: string;
tool_result: any;
created: Date;
}
export type ChatLogContent =
| ChatLogSystemContent
| ChatLogUserContent
| ChatLogAssistantContent
| ChatLogToolResultContent;
export interface ChatLog {
conversation_id: string;
continue_conversation: boolean;
content: ChatLogContent[];
created: Date;
}
// Internal wire format types (not exported)
interface ChatLogSystemContentWire {
role: "system";
content: string;
created: string;
}
interface ChatLogUserContentWire {
role: "user";
content: string;
created: string;
attachments?: ChatLogAttachment[];
}
interface ChatLogAssistantContentWire {
role: "assistant";
agent_id: string;
created: string;
content?: string;
thinking_content?: string;
tool_calls?: {
tool_name: string;
tool_args: Record<string, any>;
id: string;
external: boolean;
}[];
}
interface ChatLogToolResultContentWire {
role: "tool_result";
agent_id: string;
tool_call_id: string;
tool_name: string;
tool_result: any;
created: string;
}
type ChatLogContentWire =
| ChatLogSystemContentWire
| ChatLogUserContentWire
| ChatLogAssistantContentWire
| ChatLogToolResultContentWire;
interface ChatLogWire {
conversation_id: string;
continue_conversation: boolean;
content: ChatLogContentWire[];
created: string;
}
const processContent = (content: ChatLogContentWire): ChatLogContent => ({
...content,
created: new Date(content.created),
});
const processChatLog = (chatLog: ChatLogWire): ChatLog => ({
...chatLog,
created: new Date(chatLog.created),
content: chatLog.content.map(processContent),
});
interface ChatLogInitialStateEvent {
event_type: ChatLogEventType.INITIAL_STATE;
data: ChatLogWire;
}
interface ChatLogIndexInitialStateEvent {
event_type: ChatLogEventType.INITIAL_STATE;
data: ChatLogWire[];
}
interface ChatLogCreatedEvent {
conversation_id: string;
event_type: ChatLogEventType.CREATED;
data: ChatLogWire;
}
interface ChatLogUpdatedEvent {
conversation_id: string;
event_type: ChatLogEventType.UPDATED;
data: { chat_log: ChatLogWire };
}
interface ChatLogDeletedEvent {
conversation_id: string;
event_type: ChatLogEventType.DELETED;
data: ChatLogWire;
}
interface ChatLogContentAddedEvent {
conversation_id: string;
event_type: ChatLogEventType.CONTENT_ADDED;
data: { content: ChatLogContentWire };
}
type ChatLogSubscriptionEvent =
| ChatLogInitialStateEvent
| ChatLogUpdatedEvent
| ChatLogDeletedEvent
| ChatLogContentAddedEvent;
type ChatLogIndexSubscriptionEvent =
| ChatLogIndexInitialStateEvent
| ChatLogCreatedEvent
| ChatLogDeletedEvent;
export const subscribeChatLog = (
hass: HomeAssistant,
conversationId: string,
callback: (chatLog: ChatLog | null) => void
): Promise<UnsubscribeFunc> => {
let chatLog: ChatLog | null = null;
return hass.connection.subscribeMessage<ChatLogSubscriptionEvent>(
(event) => {
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
chatLog = processChatLog(event.data);
callback(chatLog);
} else if (event.event_type === ChatLogEventType.CONTENT_ADDED) {
if (chatLog) {
chatLog = {
...chatLog,
content: [...chatLog.content, processContent(event.data.content)],
};
callback(chatLog);
}
} else if (event.event_type === ChatLogEventType.UPDATED) {
chatLog = processChatLog(event.data.chat_log);
callback(chatLog);
} else if (event.event_type === ChatLogEventType.DELETED) {
chatLog = null;
callback(null);
}
},
{
type: "conversation/chat_log/subscribe",
conversation_id: conversationId,
}
);
};
export const subscribeChatLogIndex = (
hass: HomeAssistant,
callback: (chatLogs: ChatLog[]) => void
): Promise<UnsubscribeFunc> => {
let chatLogs: ChatLog[] = [];
return hass.connection.subscribeMessage<ChatLogIndexSubscriptionEvent>(
(event) => {
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
chatLogs = event.data.map(processChatLog);
callback(chatLogs);
} else if (event.event_type === ChatLogEventType.CREATED) {
chatLogs = [...chatLogs, processChatLog(event.data)];
callback(chatLogs);
} else if (event.event_type === ChatLogEventType.DELETED) {
chatLogs = chatLogs.filter(
(chatLog) => chatLog.conversation_id !== event.conversation_id
);
callback(chatLogs);
}
},
{
type: "conversation/chat_log/subscribe_index",
}
);
};

View File

@@ -775,7 +775,6 @@ export const getEnergyDataCollection = (
hass.locale,
hass.config
);
collection.refresh();
scheduleUpdatePeriod();
},
addHours(

View File

@@ -1,14 +0,0 @@
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

@@ -2,16 +2,16 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert";
import "../../components/ha-icon";
import "../../components/ha-md-list-item";
import "../../components/ha-list-item";
import "../../components/ha-spinner";
import type {
ExternalEntityAddToAction,
ExternalEntityAddToActions,
ExternalEntityAddToAction,
} from "../../external_app/external_messaging";
import { showToast } from "../../util/toast";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import { fireEvent } from "../../common/dom/fire_event";
@customElement("ha-more-info-add-to")
export class HaMoreInfoAddTo extends LitElement {
@@ -93,18 +93,19 @@ export class HaMoreInfoAddTo extends LitElement {
<div class="actions-list">
${this._externalActions.actions.map(
(action) => html`
<ha-md-list-item
type="button"
<ha-list-item
graphic="icon"
.disabled=${!action.enabled}
.action=${action}
.twoline=${!!action.details}
@click=${this._actionSelected}
>
<ha-icon slot="start" .icon=${action.mdi_icon}></ha-icon>
<span>${action.name}</span>
${action.details
? html`<span slot="supporting-text">${action.details}</span>`
? html`<span slot="secondary">${action.details}</span>`
: nothing}
</ha-md-list-item>
<ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
</ha-list-item>
`
)}
</div>
@@ -130,6 +131,15 @@ export class HaMoreInfoAddTo extends LitElement {
flex-direction: column;
}
ha-list-item {
cursor: pointer;
}
ha-list-item[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
ha-icon {
display: flex;
align-items: center;

View File

@@ -7,7 +7,6 @@ import { listenMediaQuery } from "../common/dom/media_query";
import { toggleAttribute } from "../common/dom/toggle_attribute";
import { computeRTLDirection } from "../common/util/compute_rtl";
import "../components/ha-drawer";
import "../components/ha-snowflakes";
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
import type { HomeAssistant, Route } from "../types";
import "./partial-panel-resolver";
@@ -51,7 +50,6 @@ export class HomeAssistantMain extends LitElement {
this.hass.panels && this.hass.userData && this.hass.systemData;
return html`
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
<ha-drawer
.type=${sidebarNarrow ? "modal" : ""}
.open=${sidebarNarrow ? this._drawerOpen : false}

View File

@@ -1,10 +1,14 @@
import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-analytics";
import "../../../components/ha-button";
import "../../../components/ha-card";
import "../../../components/ha-checkbox";
import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon";
import type { Analytics } from "../../../data/analytics";
import {
getAnalyticsDetails,
@@ -13,8 +17,6 @@ import {
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-alert";
@customElement("ha-config-analytics")
class ConfigAnalytics extends LitElement {
@@ -32,22 +34,10 @@ class ConfigAnalytics extends LitElement {
: undefined;
return html`
<ha-card
outlined
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
"Home Assistant analytics"}
>
<ha-card outlined>
<div class="card-content">
${error ? html`<div class="error">${error}</div>` : nothing}
<p>
${this.hass.localize("ui.panel.config.analytics.intro")}
<a
href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.config.analytics.learn_more")}</a
>.
</p>
${error ? html`<div class="error">${error}</div>` : ""}
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
<ha-analytics
translation_key_panel="config"
@analytics-preferences-changed=${this._preferencesChanged}
@@ -55,59 +45,26 @@ class ConfigAnalytics extends LitElement {
.analytics=${this._analyticsDetails}
></ha-analytics>
</div>
</ha-card>
${this._analyticsDetails &&
"snapshots" in this._analyticsDetails.preferences
? html`<ha-card
outlined
.header=${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.header"
)}
>
<div class="card-content">
<p>
<div class="card-actions">
<ha-button @click=${this._save}>
${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.info"
"ui.panel.config.core.section.core.core_config.save_button"
)}
<a
href=${documentationUrl(this.hass, "/device-database/")}
</ha-button>
</div>
</ha-card>
<div class="footer">
<ha-button
size="small"
appearance="plain"
href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank"
rel="noreferrer"
>${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.learn_more"
)}</a
>.
</p>
<ha-alert
.title=${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.alert.title"
)}
>${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.alert.content"
)}</ha-alert
>
<ha-settings-row>
<span slot="heading" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.title`
)}
</span>
<span slot="description" data-for="snapshots">
${this.hass.localize(
`ui.panel.config.analytics.preferences.snapshots.description`
)}
</span>
<ha-switch
@change=${this._handleDeviceRowClick}
.checked=${!!this._analyticsDetails?.preferences.snapshots}
.disabled=${this._analyticsDetails === undefined}
name="snapshots"
>
</ha-switch>
</ha-settings-row>
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
${this.hass.localize("ui.panel.config.analytics.learn_more")}
</ha-button>
</div>
</ha-card>`
: nothing}
`;
}
@@ -139,25 +96,11 @@ class ConfigAnalytics extends LitElement {
}
}
private _handleDeviceRowClick(ev: Event) {
const target = ev.target as HaSwitch;
this._analyticsDetails = {
...this._analyticsDetails!,
preferences: {
...this._analyticsDetails!.preferences,
snapshots: target.checked,
},
};
this._save();
}
private _preferencesChanged(event: CustomEvent): void {
this._analyticsDetails = {
...this._analyticsDetails!,
preferences: event.detail.preferences,
};
this._save();
}
static get styles(): CSSResultGroup {
@@ -174,10 +117,21 @@ class ConfigAnalytics extends LitElement {
p {
margin-top: 0;
}
ha-card:not(:first-of-type) {
margin-top: 24px;
.card-actions {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
align-items: center;
}
`,
.footer {
padding: 32px 0 16px;
text-align: center;
}
ha-button[size="small"] ha-svg-icon {
--mdc-icon-size: 16px;
}
`, // row-reverse so we tab first to "save"
];
}
}

View File

@@ -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,

View File

@@ -1,52 +0,0 @@
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,17 +1162,6 @@ 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

@@ -25,6 +25,10 @@ import { computeCssColor } from "../../../common/color/compute-color";
import { storage } from "../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import {
DEFAULT_ENTITY_NAME,
type EntityNameItem,
} from "../../../common/entity/compute_entity_name_display";
import { navigate } from "../../../common/navigate";
import type {
LocalizeFunc,
@@ -122,6 +126,11 @@ import {
import { getSignedPath } from "../../../data/auth";
import { fileDownload } from "../../../util/file_download";
const HELPER_ENTITY_NAME: EntityNameItem[] = [
{ type: "area" },
...DEFAULT_ENTITY_NAME,
];
interface HelperItem {
id: string;
name: string;
@@ -505,7 +514,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
return {
id: entityState.entity_id,
name: entityState.attributes.friendly_name || "",
name: this._formatHelperName(entityState),
entity_id: entityState.entity_id,
editable:
configEntry !== undefined || entityState.attributes.editable,
@@ -584,6 +593,14 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
}
);
private _formatHelperName(stateObj: HassEntity): string {
const formatted =
this.hass.formatEntityName(stateObj, HELPER_ENTITY_NAME) || "";
return (
formatted || stateObj.attributes.friendly_name || stateObj.entity_id || ""
);
}
private _labelsForEntity(entityId: string): string[] {
return (
this.hass.entities[entityId]?.labels ||

View File

@@ -1,137 +0,0 @@
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,
var(--secondary-background-color)
);
padding: var(--ha-space-3);
overflow: auto;
}
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

@@ -1,20 +0,0 @@
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

@@ -6,7 +6,6 @@ import {
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
import type {
PipelineRunEvent,
@@ -21,8 +20,6 @@ import "../../../../layouts/hass-subpage";
import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant, Route } from "../../../../types";
import "./assist-render-pipeline-events";
import type { ChatLog } from "../../../../data/chat_log";
import { subscribeChatLog } from "../../../../data/chat_log";
@customElement("assist-pipeline-debug")
export class AssistPipelineDebug extends LitElement {
@@ -40,12 +37,8 @@ export class AssistPipelineDebug extends LitElement {
@state() private _events?: PipelineRunEvent[];
@state() private _chatLog?: ChatLog;
private _unsubRefreshEventsID?: number;
private _unsubChatLogUpdates?: Promise<UnsubscribeFunc>;
protected render() {
return html`<hass-subpage
.narrow=${this.narrow}
@@ -113,7 +106,6 @@ export class AssistPipelineDebug extends LitElement {
? html`<assist-render-pipeline-events
.hass=${this.hass}
.events=${this._events}
.chatLog=${this._chatLog}
></assist-render-pipeline-events>`
: ""}
</div>
@@ -128,10 +120,6 @@ export class AssistPipelineDebug extends LitElement {
clearRefresh = true;
}
if (changedProperties.has("_runId")) {
if (this._unsubChatLogUpdates) {
this._unsubChatLogUpdates.then((unsub) => unsub());
this._unsubChatLogUpdates = undefined;
}
this._fetchEvents();
clearRefresh = true;
}
@@ -147,10 +135,6 @@ export class AssistPipelineDebug extends LitElement {
clearTimeout(this._unsubRefreshEventsID);
this._unsubRefreshEventsID = undefined;
}
if (this._unsubChatLogUpdates) {
this._unsubChatLogUpdates.then((unsub) => unsub());
this._unsubChatLogUpdates = undefined;
}
}
private async _fetchRuns() {
@@ -201,27 +185,8 @@ export class AssistPipelineDebug extends LitElement {
});
return;
}
if (!this._events!.length) {
return;
}
if (!this._unsubChatLogUpdates && this._events[0].type === "run-start") {
this._unsubChatLogUpdates = subscribeChatLog(
this.hass,
this._events[0].data.conversation_id,
(chatLog) => {
if (chatLog) {
this._chatLog = chatLog;
} else {
this._unsubChatLogUpdates?.then((unsub) => unsub());
this._unsubChatLogUpdates = undefined;
}
}
);
this._unsubChatLogUpdates.catch(() => {
this._unsubChatLogUpdates = undefined;
});
}
if (
this._events?.length &&
// If the last event is not a finish run event, the run is still ongoing.
// Refresh events automatically.
!["run-end", "error"].includes(this._events[this._events.length - 1].type)

View File

@@ -1,7 +1,6 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { extractSearchParam } from "../../../../common/url/search-params";
import "../../../../components/ha-assist-pipeline-picker";
import "../../../../components/ha-button";
@@ -25,8 +24,6 @@ import type { HomeAssistant } from "../../../../types";
import { AudioRecorder } from "../../../../util/audio-recorder";
import { fileDownload } from "../../../../util/file_download";
import "./assist-render-pipeline-run";
import type { ChatLog } from "../../../../data/chat_log";
import { subscribeChatLog } from "../../../../data/chat_log";
@customElement("assist-pipeline-run-debug")
export class AssistPipelineRunDebug extends LitElement {
@@ -49,13 +46,6 @@ export class AssistPipelineRunDebug extends LitElement {
@state() private _pipelineId?: string =
extractSearchParam("pipeline") || undefined;
@state() private _chatLog?: ChatLog;
private _chatLogSubscription: {
conversationId: string;
unsub: Promise<UnsubscribeFunc>;
} | null = null;
protected render(): TemplateResult {
return html`
<hass-subpage
@@ -188,7 +178,6 @@ export class AssistPipelineRunDebug extends LitElement {
<assist-render-pipeline-run
.hass=${this.hass}
.pipelineRun=${run}
.chatLog=${this._chatLog}
></assist-render-pipeline-run>
`
)}
@@ -197,14 +186,6 @@ export class AssistPipelineRunDebug extends LitElement {
`;
}
public disconnectedCallback(): void {
super.disconnectedCallback();
if (this._chatLogSubscription) {
this._chatLogSubscription.unsub.then((unsub) => unsub());
this._chatLogSubscription = null;
}
}
private get conversationId(): string | null {
return this._pipelineRuns.length === 0
? null
@@ -427,32 +408,6 @@ export class AssistPipelineRunDebug extends LitElement {
added = true;
}
callback(updatedRun);
const conversationId = this.conversationId;
if (
!this._chatLog &&
conversationId &&
(!this._chatLogSubscription ||
this._chatLogSubscription.conversationId !== conversationId)
) {
if (this._chatLogSubscription) {
this._chatLogSubscription.unsub.then((unsub) => unsub());
}
this._chatLogSubscription = {
conversationId,
unsub: subscribeChatLog(this.hass, conversationId, (chatLog) => {
if (chatLog) {
this._chatLog = chatLog;
} else {
this._chatLogSubscription?.unsub.then((unsub) => unsub());
this._chatLogSubscription = null;
}
}),
};
this._chatLogSubscription.unsub.catch(() => {
this._chatLogSubscription = null;
});
}
},
{
...options,

View File

@@ -9,7 +9,6 @@ import type {
import { processEvent } from "../../../../data/assist_pipeline";
import type { HomeAssistant } from "../../../../types";
import "./assist-render-pipeline-run";
import type { ChatLog } from "../../../../data/chat_log";
@customElement("assist-render-pipeline-events")
export class AssistPipelineEvents extends LitElement {
@@ -17,8 +16,6 @@ export class AssistPipelineEvents extends LitElement {
@property({ attribute: false }) public events!: PipelineRunEvent[];
@property({ attribute: false }) public chatLog?: ChatLog;
private _processEvents = memoizeOne(
(events: PipelineRunEvent[]): PipelineRun | undefined => {
let run: PipelineRun | undefined;
@@ -59,7 +56,6 @@ export class AssistPipelineEvents extends LitElement {
<assist-render-pipeline-run
.hass=${this.hass}
.pipelineRun=${run}
.chatLog=${this.chatLog}
></assist-render-pipeline-run>
`;
}

View File

@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../../components/ha-card";
import "../../../../components/ha-alert";
@@ -12,12 +12,6 @@ import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/ha-yaml-editor";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { LocalizeKeys } from "../../../../common/translations/localize";
import type {
ChatLogAssistantContent,
ChatLog,
ChatLogContent,
ChatLogUserContent,
} from "../../../../data/chat_log";
const RUN_DATA = ["pipeline", "language"];
const WAKE_WORD_DATA = ["engine"];
@@ -125,7 +119,7 @@ const dataMinusKeysRender = (
result[key] = data[key];
}
return render
? html`<ha-expansion-panel class="yaml-expansion">
? html`<ha-expansion-panel>
<span slot="header"
>${hass.localize("ui.panel.config.voice_assistants.debug.raw")}</span
>
@@ -140,8 +134,6 @@ export class AssistPipelineDebug extends LitElement {
@property({ attribute: false }) public pipelineRun!: PipelineRun;
@property({ attribute: false }) public chatLog?: ChatLog;
private _audioElement?: HTMLAudioElement;
private get _isPlaying(): boolean {
@@ -155,23 +147,8 @@ export class AssistPipelineDebug extends LitElement {
) || "ready"
: "ready";
let messages: ChatLogContent[];
const messages: { from: string; text: string }[] = [];
if (this.chatLog) {
messages = this.chatLog.content.filter(
this.pipelineRun.finished
? (content: ChatLogContent) =>
content.role === "system" ||
(content.created >= this.pipelineRun.started &&
content.created <= this.pipelineRun.finished!)
: (content: ChatLogContent) =>
content.role === "system" ||
content.created >= this.pipelineRun.started
);
} else {
messages = [];
// We don't have the chat log everywhere yet, just fallback for now.
const userMessage =
(this.pipelineRun.init_options &&
"text" in this.pipelineRun.init_options.input
@@ -182,20 +159,19 @@ export class AssistPipelineDebug extends LitElement {
if (userMessage) {
messages.push({
role: "user",
content: userMessage,
} as ChatLogUserContent);
from: "user",
text: userMessage,
});
}
if (
this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
) {
messages.push({
role: "assistant",
content:
this.pipelineRun.intent.intent_output.response.speech.plain.speech,
} as ChatLogAssistantContent);
}
from: "hass",
text: this.pipelineRun.intent.intent_output.response.speech.plain
.speech,
});
}
return html`
@@ -214,57 +190,9 @@ export class AssistPipelineDebug extends LitElement {
${messages.length > 0
? html`
<div class="messages">
${messages.map((content) =>
content.role === "system" || content.role === "tool_result"
? html`
<ha-expansion-panel
class="content-expansion ${content.role}"
>
<div slot="header">
${content.role === "system"
? "System"
: `Result for ${content.tool_name}`}
</div>
${content.role === "system"
? html`<pre>${content.content}</pre>`
: html`
<ha-yaml-editor
read-only
auto-update
.value=${content}
></ha-yaml-editor>
`}
</ha-expansion-panel>
`
: html`
${content.content
? html`
<div class=${`message ${content.role}`}>
${content.content}
</div>
`
: nothing}
${content.role === "assistant" &&
content.tool_calls?.length
? html`
<ha-expansion-panel
class="content-expansion assistant"
>
<span slot="header">
Call
${content.tool_calls.length === 1
? content.tool_calls[0].tool_name
: `${content.tool_calls.length} tools`}
</span>
<ha-yaml-editor
read-only
auto-update
.value=${content.tool_calls}
></ha-yaml-editor>
</ha-expansion-panel>
`
: nothing}
${messages.map(
({ from, text }) => html`
<div class=${`message ${from}`}>${text}</div>
`
)}
</div>
@@ -514,7 +442,7 @@ export class AssistPipelineDebug extends LitElement {
: ""}
${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
<ha-card>
<ha-expansion-panel class="yaml-expansion">
<ha-expansion-panel>
<span slot="header"
>${this.hass.localize(
"ui.panel.config.voice_assistants.debug.raw"
@@ -591,12 +519,12 @@ export class AssistPipelineDebug extends LitElement {
.row > div:last-child {
text-align: right;
}
.yaml-expansion {
ha-expansion-panel {
padding-left: 8px;
padding-inline-start: 8px;
padding-inline-end: initial;
}
.card-content .yaml-expansion {
.card-content ha-expansion-panel {
padding-left: 0px;
padding-inline-start: 0px;
padding-inline-end: initial;
@@ -612,59 +540,27 @@ export class AssistPipelineDebug extends LitElement {
margin-top: 8px;
}
.content-expansion {
margin: 8px 0;
border-radius: var(--ha-border-radius-xl);
clear: both;
padding: 0 8px;
--input-fill-color: none;
max-width: calc(100% - 24px);
--expansion-panel-summary-padding: 0px;
--expansion-panel-content-padding: 0px;
}
.content-expansion *[slot="header"] {
font-weight: var(--ha-font-weight-normal);
}
.system {
background-color: var(--success-color);
}
.message {
padding: 8px;
}
.message,
.content-expansion {
font-size: var(--ha-font-size-l);
margin: 8px 0;
padding: 8px;
border-radius: var(--ha-border-radius-xl);
clear: both;
}
.messages pre {
white-space: pre-wrap;
}
.user,
.tool_result {
.message.user {
margin-left: 24px;
margin-inline-start: 24px;
margin-inline-end: initial;
float: var(--float-end);
text-align: right;
border-bottom-right-radius: 0px;
background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color));
direction: var(--direction);
}
.message.user,
.content-expansion div[slot="header"] {
text-align: right;
}
.assistant {
.message.hass {
margin-right: 24px;
margin-inline-end: 24px;
margin-inline-start: initial;

View File

@@ -5412,11 +5412,6 @@
"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": {
@@ -6785,7 +6780,6 @@
},
"analytics": {
"caption": "Analytics",
"header": "Home Assistant analytics",
"description": "Learn how to share data to improve Home Assistant",
"preferences": {
"base": {
@@ -6803,21 +6797,10 @@
"diagnostics": {
"title": "Diagnostics",
"description": "Share crash reports when unexpected errors occur."
},
"snapshots": {
"title": "Devices",
"description": "Generic information about your devices.",
"header": "Device analytics",
"info": "Anonymously share data about your devices to help build the Open Home Foundations device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).",
"learn_more": "Learn more about the device database and how we process your data",
"alert": {
"title": "Important",
"content": "Only enable this option if you understand that your device information will be shared."
}
}
},
"need_base_enabled": "You need to enable basic analytics for this option to be available",
"learn_more": "Learn how we process your data",
"learn_more": "How we process your data",
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
"download_device_info": "Preview device analytics"
},

View File

@@ -9332,7 +9332,7 @@ __metadata:
gulp-rename: "npm:2.1.0"
gulp-zopfli-green: "npm:6.0.2"
hls.js: "npm:1.6.14"
home-assistant-js-websocket: "npm:9.6.0"
home-assistant-js-websocket: "npm:9.5.0"
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.2"
@@ -9393,10 +9393,10 @@ __metadata:
languageName: unknown
linkType: soft
"home-assistant-js-websocket@npm:9.6.0":
version: 9.6.0
resolution: "home-assistant-js-websocket@npm:9.6.0"
checksum: 10/0eded7864632b5e19e92289ffac0e24308b1e8f425e292ae87ed21450852f7705db521e202614b1d5bbdb7948633143dce2524ed548db0c38486b40ed1ffa474
"home-assistant-js-websocket@npm:9.5.0":
version: 9.5.0
resolution: "home-assistant-js-websocket@npm:9.5.0"
checksum: 10/42f991b3b85aa61be28984f099a001ac083fb3da54b2777283d0c97976c564a303d8d4ba467e1b8e29cbc33151cd6eef64c1a7d3392d62bbb9cbb27aa7ca9942
languageName: node
linkType: hard