Compare commits

..

9 Commits

Author SHA1 Message Date
Simon Lamon
9a45fbc488 Merge branch 'dev' into sec_pypi_publishing 2025-11-23 17:01:26 +01:00
Simon Lamon
aef3cb1c36 Merge branch 'dev' into sec_pypi_publishing 2025-11-20 09:53:07 +01:00
Simon Lamon
8535ee0694 Merge branch 'dev' into sec_pypi_publishing 2025-11-13 06:58:30 +01:00
Simon Lamon
b8110d1a45 Merge branch 'dev' into sec_pypi_publishing 2025-10-27 06:41:51 +01:00
Simon Lamon
19e9de39c5 Merge branch 'dev' into sec_pypi_publishing 2025-10-19 10:56:12 +02:00
Simon Lamon
f22f01e513 Merge branch 'dev' into sec_pypi_publishing 2025-10-06 20:28:38 +02:00
Simon Lamon
3f86f144b5 Merge branch 'dev' into sec_pypi_publishing 2025-10-04 17:25:20 +02:00
Simon Lamon
4efef5ed16 Update release.yaml 2025-09-24 07:04:06 +02:00
Simon Lamon
cac7ae2a40 Remove twine and introduce trusted publishing 2025-09-20 21:23:04 +02:00
41 changed files with 197 additions and 1334 deletions

View File

@@ -21,7 +21,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: dev ref: dev
@@ -56,7 +56,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: master ref: master

View File

@@ -24,7 +24,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
@@ -58,7 +58,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
@@ -76,7 +76,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
@@ -100,7 +100,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:

View File

@@ -23,7 +23,7 @@ jobs:
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
# We must fetch at least the immediate parents so that if this is # We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head. # a pull request then we can checkout the head.
@@ -36,14 +36,14 @@ jobs:
# Initializes the CodeQL tools for scanning. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - name: Autobuild
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
# Command-line programs to run using the OS shell. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -57,4 +57,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4 uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3

View File

@@ -22,7 +22,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: dev ref: dev
@@ -57,7 +57,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with: with:
ref: master ref: master

View File

@@ -16,7 +16,7 @@ jobs:
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0

View File

@@ -21,7 +21,7 @@ jobs:
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
steps: steps:
- name: Check out files from GitHub - name: Check out files from GitHub
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0

View File

@@ -20,7 +20,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6

View File

@@ -19,11 +19,14 @@ jobs:
release: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
environment: pypi
permissions: permissions:
contents: write # Required to upload release assets contents: write # Required to upload release assets
id-token: write # For "Trusted Publisher" to PyPi
if: github.repository_owner == 'home-assistant'
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up Python ${{ env.PYTHON_VERSION }} - name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0 uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
@@ -46,14 +49,18 @@ jobs:
run: ./script/translations_download run: ./script/translations_download
env: env:
LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }} LOKALISE_TOKEN: ${{ secrets.LOKALISE_TOKEN }}
- name: Build and release package - name: Build and release package
run: | run: |
python3 -m pip install twine build python3 -m pip install build
export TWINE_USERNAME="__token__"
export TWINE_PASSWORD="${{ secrets.TWINE_TOKEN }}"
export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1 export SKIP_FETCH_NIGHTLY_TRANSLATIONS=1
script/release script/release
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
skip-existing: true
- name: Upload release assets - name: Upload release assets
uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2 uses: softprops/action-gh-release@5be0e66d93ac7ed76da52eca8bb058f665c3a5fe # v2.4.2
with: with:
@@ -91,7 +98,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:
@@ -120,7 +127,7 @@ jobs:
contents: write # Required to upload release assets contents: write # Required to upload release assets
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Setup Node - name: Setup Node
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
with: with:

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout the repository - name: Checkout the repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Upload Translations - name: Upload Translations
run: | run: |

View File

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

View File

@@ -1,5 +1,4 @@
#!/bin/sh #!/bin/sh
# Pushes a new version to PyPi.
# Stop on errors # Stop on errors
set -e set -e
@@ -12,5 +11,4 @@ yarn install
script/build_frontend script/build_frontend
rm -rf dist home_assistant_frontend.egg-info rm -rf dist home_assistant_frontend.egg-info
python3 -m build python3 -m build -q
python3 -m twine upload dist/*.whl --skip-existing

View File

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

View File

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

View File

@@ -209,7 +209,6 @@ export class HaExpansionPanel extends LitElement {
::slotted([slot="header"]) { ::slotted([slot="header"]) {
flex: 1; flex: 1;
overflow-wrap: anywhere; overflow-wrap: anywhere;
color: var(--primary-text-color);
} }
.container { .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; diagnostics?: boolean;
usage?: boolean; usage?: boolean;
statistics?: boolean; statistics?: boolean;
snapshots?: boolean;
} }
export interface Analytics { export interface Analytics {

View File

@@ -214,8 +214,6 @@ export interface PipelineRun {
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error"; stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
run: PipelineRunStartEvent["data"]; run: PipelineRunStartEvent["data"];
error?: PipelineErrorEvent["data"]; error?: PipelineErrorEvent["data"];
started: Date;
finished?: Date;
wake_word?: PipelineWakeWordStartEvent["data"] & wake_word?: PipelineWakeWordStartEvent["data"] &
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean }; Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
stt?: PipelineSTTStartEvent["data"] & stt?: PipelineSTTStartEvent["data"] &
@@ -237,7 +235,6 @@ export const processEvent = (
stage: "ready", stage: "ready",
run: event.data, run: event.data,
events: [event], events: [event],
started: new Date(event.timestamp),
}; };
return run; return run;
} }
@@ -293,14 +290,9 @@ export const processEvent = (
tts: { ...run.tts!, ...event.data, done: true }, tts: { ...run.tts!, ...event.data, done: true },
}; };
} else if (event.type === "run-end") { } else if (event.type === "run-end") {
run = { ...run, finished: new Date(event.timestamp), stage: "done" }; run = { ...run, stage: "done" };
} else if (event.type === "error") { } else if (event.type === "error") {
run = { run = { ...run, stage: "error", error: event.data };
...run,
finished: new Date(event.timestamp),
stage: "error",
error: event.data,
};
} else { } else {
run = { ...run }; 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.locale,
hass.config hass.config
); );
collection.refresh();
scheduleUpdatePeriod(); scheduleUpdatePeriod();
}, },
addHours( 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,15 +2,14 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert"; import "../../components/ha-alert";
import "../../components/ha-icon"; import "../../components/ha-icon";
import "../../components/ha-md-list-item"; import "../../components/ha-list-item";
import "../../components/ha-spinner"; import "../../components/ha-spinner";
import type { import type {
ExternalEntityAddToAction,
ExternalEntityAddToActions, ExternalEntityAddToActions,
ExternalEntityAddToAction,
} from "../../external_app/external_messaging"; } from "../../external_app/external_messaging";
import { showToast } from "../../util/toast"; import { showToast } from "../../util/toast";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types"; import type { HomeAssistant } from "../../types";
@customElement("ha-more-info-add-to") @customElement("ha-more-info-add-to")
@@ -52,7 +51,6 @@ export class HaMoreInfoAddTo extends LitElement {
app_payload: action.app_payload, app_payload: action.app_payload,
}, },
}); });
fireEvent(this, "add-to-action-selected");
} catch (err: any) { } catch (err: any) {
showToast(this, { showToast(this, {
message: this.hass.localize( message: this.hass.localize(
@@ -93,18 +91,19 @@ export class HaMoreInfoAddTo extends LitElement {
<div class="actions-list"> <div class="actions-list">
${this._externalActions.actions.map( ${this._externalActions.actions.map(
(action) => html` (action) => html`
<ha-md-list-item <ha-list-item
type="button" graphic="icon"
.disabled=${!action.enabled} .disabled=${!action.enabled}
.action=${action} .action=${action}
.twoline=${!!action.details}
@click=${this._actionSelected} @click=${this._actionSelected}
> >
<ha-icon slot="start" .icon=${action.mdi_icon}></ha-icon>
<span>${action.name}</span> <span>${action.name}</span>
${action.details ${action.details
? html`<span slot="supporting-text">${action.details}</span>` ? html`<span slot="secondary">${action.details}</span>`
: nothing} : nothing}
</ha-md-list-item> <ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
</ha-list-item>
` `
)} )}
</div> </div>
@@ -130,6 +129,15 @@ export class HaMoreInfoAddTo extends LitElement {
flex-direction: column; flex-direction: column;
} }
ha-list-item {
cursor: pointer;
}
ha-list-item[disabled] {
cursor: not-allowed;
opacity: 0.5;
}
ha-icon { ha-icon {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -141,8 +149,4 @@ declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-more-info-add-to": HaMoreInfoAddTo; "ha-more-info-add-to": HaMoreInfoAddTo;
} }
interface HASSDomEvents {
"add-to-action-selected": undefined;
}
} }

View File

@@ -645,7 +645,6 @@ export class MoreInfoDialog extends LitElement {
<ha-more-info-add-to <ha-more-info-add-to
.hass=${this.hass} .hass=${this.hass}
.entityId=${entityId} .entityId=${entityId}
@add-to-action-selected=${this._goBack}
></ha-more-info-add-to> ></ha-more-info-add-to>
` `
: nothing : nothing

View File

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

View File

@@ -188,7 +188,6 @@ export default class HaAutomationSidebar extends LitElement {
class="handle ${this._resizing ? "resizing" : ""}" class="handle ${this._resizing ? "resizing" : ""}"
@mousedown=${this._handleMouseDown} @mousedown=${this._handleMouseDown}
@touchstart=${this._handleMouseDown} @touchstart=${this._handleMouseDown}
@dblclick=${this._handleDoubleClick}
@focus=${this._startKeyboardResizing} @focus=${this._startKeyboardResizing}
@blur=${this._stopKeyboardResizing} @blur=${this._stopKeyboardResizing}
tabindex="0" tabindex="0"
@@ -259,17 +258,6 @@ export default class HaAutomationSidebar extends LitElement {
); );
}; };
private _handleDoubleClick = (ev: MouseEvent) => {
ev.preventDefault();
ev.stopPropagation();
this._unregisterResizeHandlers();
this._tinykeysUnsub?.();
this._tinykeysUnsub = undefined;
this._resizing = false;
document.body.style.removeProperty("cursor");
fireEvent(this, "sidebar-reset-size");
};
private _startResizing(clientX: number) { private _startResizing(clientX: number) {
// register event listeners for drag handling // register event listeners for drag handling
document.addEventListener("mousemove", this._handleMouseMove); document.addEventListener("mousemove", this._handleMouseMove);
@@ -434,6 +422,5 @@ declare global {
deltaInPx: number; deltaInPx: number;
}; };
"sidebar-resizing-stopped": undefined; "sidebar-resizing-stopped": undefined;
"sidebar-reset-size": undefined;
} }
} }

View File

@@ -317,7 +317,6 @@ export class HaManualAutomationEditor extends LitElement {
@value-changed=${this._sidebarConfigChanged} @value-changed=${this._sidebarConfigChanged}
@sidebar-resized=${this._resizeSidebar} @sidebar-resized=${this._resizeSidebar}
@sidebar-resizing-stopped=${this._stopResizeSidebar} @sidebar-resizing-stopped=${this._stopResizeSidebar}
@sidebar-reset-size=${this._resetSidebarWidth}
></ha-automation-sidebar> ></ha-automation-sidebar>
</div> </div>
</div> </div>
@@ -701,16 +700,6 @@ export class HaManualAutomationEditor extends LitElement {
this._prevSidebarWidthPx = undefined; this._prevSidebarWidthPx = undefined;
} }
private _resetSidebarWidth(ev: Event) {
ev.stopPropagation();
this._prevSidebarWidthPx = undefined;
this._sidebarWidthPx = SIDEBAR_DEFAULT_WIDTH;
this.style.setProperty(
"--sidebar-dynamic-width",
`${this._sidebarWidthPx}px`
);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
saveFabStyles, saveFabStyles,

View File

@@ -43,7 +43,6 @@ import {
} from "../../../data/blueprint"; } from "../../../data/blueprint";
import { showScriptEditor } from "../../../data/script"; import { showScriptEditor } from "../../../data/script";
import { findRelated } from "../../../data/search"; import { findRelated } from "../../../data/search";
import "../../../components/chips/ha-assist-chip";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@@ -61,7 +60,6 @@ type BlueprintMetaDataPath = BlueprintMetaData & {
error: boolean; error: boolean;
type: "automation" | "script"; type: "automation" | "script";
fullpath: string; fullpath: string;
usageCount?: number;
}; };
const createNewFunctions = { const createNewFunctions = {
@@ -130,20 +128,14 @@ class HaBlueprintOverview extends LitElement {
}) })
private _filter = ""; private _filter = "";
@state() private _usageCounts: Record<string, number> = {};
private _usageCountRequest = 0;
private _processedBlueprints = memoizeOne( private _processedBlueprints = memoizeOne(
( (
blueprints: Record<string, Blueprints>, blueprints: Record<string, Blueprints>,
localize: LocalizeFunc, localize: LocalizeFunc
usageCounts: Record<string, number>
): BlueprintMetaDataPath[] => { ): BlueprintMetaDataPath[] => {
const result: any[] = []; const result: any[] = [];
Object.entries(blueprints).forEach(([type, typeBlueprints]) => Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
Object.entries(typeBlueprints).forEach(([path, blueprint]) => { Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
const fullpath = `${type}/${path}`;
if ("error" in blueprint) { if ("error" in blueprint) {
result.push({ result.push({
name: blueprint.error, name: blueprint.error,
@@ -153,8 +145,7 @@ class HaBlueprintOverview extends LitElement {
), ),
error: true, error: true,
path, path,
fullpath, fullpath: `${type}/${path}`,
usageCount: 0,
}); });
} else { } else {
result.push({ result.push({
@@ -165,8 +156,7 @@ class HaBlueprintOverview extends LitElement {
), ),
error: false, error: false,
path, path,
fullpath, fullpath: `${type}/${path}`,
usageCount: usageCounts[fullpath] || 0,
}); });
} }
}) })
@@ -199,34 +189,6 @@ class HaBlueprintOverview extends LitElement {
filterable: true, filterable: true,
flex: 2, flex: 2,
}, },
usage_count: {
title: localize(
"ui.panel.config.blueprint.overview.headers.usage_count"
),
sortable: true,
valueColumn: "usageCount",
type: "numeric",
minWidth: "100px",
maxWidth: "120px",
template: (blueprint) => {
const count = blueprint.usageCount ?? 0;
return html`
<ha-assist-chip
filled
.active=${count > 0}
label=${String(count)}
title=${blueprint.error
? String(count)
: this.hass.localize(
`ui.panel.config.blueprint.overview.view_${blueprint.type}`
)}
?disabled=${blueprint.error}
data-fullpath=${blueprint.fullpath}
@click=${this._handleUsageClick}
></ha-assist-chip>
`;
},
},
fullpath: { fullpath: {
title: "fullpath", title: "fullpath",
hidden: true, hidden: true,
@@ -304,7 +266,6 @@ class HaBlueprintOverview extends LitElement {
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
this._loadUsageCounts();
if (this.route.path === "/import") { if (this.route.path === "/import") {
const url = extractSearchParam("blueprint_url"); const url = extractSearchParam("blueprint_url");
navigate("/config/blueprint/dashboard", { replace: true }); navigate("/config/blueprint/dashboard", { replace: true });
@@ -314,13 +275,6 @@ class HaBlueprintOverview extends LitElement {
} }
} }
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
if (changedProps.has("blueprints")) {
this._loadUsageCounts();
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<hass-tabs-subpage-data-table <hass-tabs-subpage-data-table
@@ -330,11 +284,7 @@ class HaBlueprintOverview extends LitElement {
.route=${this.route} .route=${this.route}
.tabs=${configSections.automations} .tabs=${configSections.automations}
.columns=${this._columns(this.hass.localize)} .columns=${this._columns(this.hass.localize)}
.data=${this._processedBlueprints( .data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
this.blueprints,
this.hass.localize,
this._usageCounts
)}
id="fullpath" id="fullpath"
.noDataText=${this.hass.localize( .noDataText=${this.hass.localize(
"ui.panel.config.blueprint.overview.no_blueprints" "ui.panel.config.blueprint.overview.no_blueprints"
@@ -430,51 +380,10 @@ class HaBlueprintOverview extends LitElement {
fireEvent(this, "reload-blueprints"); fireEvent(this, "reload-blueprints");
} }
private async _loadUsageCounts() {
if (!this.blueprints) {
return;
}
const request = ++this._usageCountRequest;
const usageCounts: Record<string, number> = {};
const blueprintList = this._processedBlueprints(
this.blueprints,
this.hass.localize,
{}
);
await Promise.all(
blueprintList.map(async (blueprint) => {
if (blueprint.error) {
usageCounts[blueprint.fullpath] = 0;
return;
}
try {
const related = await findRelated(
this.hass,
`${blueprint.domain}_blueprint`,
blueprint.path
);
const count =
(related.automation?.length || 0) + (related.script?.length || 0);
usageCounts[blueprint.fullpath] = count;
} catch (_err) {
usageCounts[blueprint.fullpath] = 0;
}
})
);
if (request === this._usageCountRequest) {
this._usageCounts = usageCounts;
}
}
private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) {
const blueprint = this._processedBlueprints( const blueprint = this._processedBlueprints(
this.blueprints, this.blueprints,
this.hass.localize, this.hass.localize
this._usageCounts
).find((b) => b.fullpath === ev.detail.id)!; ).find((b) => b.fullpath === ev.detail.id)!;
if (blueprint.error) { if (blueprint.error) {
showAlertDialog(this, { showAlertDialog(this, {
@@ -488,25 +397,6 @@ class HaBlueprintOverview extends LitElement {
this._createNew(blueprint); this._createNew(blueprint);
} }
private _handleUsageClick = (ev: Event) => {
ev.stopPropagation();
ev.preventDefault();
const target = ev.currentTarget as HTMLElement | null;
const fullpath = target?.dataset.fullpath;
if (!fullpath) {
return;
}
const blueprint = this._processedBlueprints(
this.blueprints,
this.hass.localize,
this._usageCounts
).find((item) => item.fullpath === fullpath);
if (!blueprint || blueprint.error) {
return;
}
this._showUsed(blueprint);
};
private _showUsed = (blueprint: BlueprintMetaDataPath) => { private _showUsed = (blueprint: BlueprintMetaDataPath) => {
navigate( navigate(
`/config/${blueprint.domain}/dashboard?blueprint=${encodeURIComponent( `/config/${blueprint.domain}/dashboard?blueprint=${encodeURIComponent(

View File

@@ -1,10 +1,14 @@
import { mdiOpenInNew } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; 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 { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import "../../../components/ha-analytics"; import "../../../components/ha-analytics";
import "../../../components/ha-button";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-checkbox";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
import "../../../components/ha-svg-icon";
import type { Analytics } from "../../../data/analytics"; import type { Analytics } from "../../../data/analytics";
import { import {
getAnalyticsDetails, getAnalyticsDetails,
@@ -13,8 +17,6 @@ import {
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url"; import { documentationUrl } from "../../../util/documentation-url";
import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-alert";
@customElement("ha-config-analytics") @customElement("ha-config-analytics")
class ConfigAnalytics extends LitElement { class ConfigAnalytics extends LitElement {
@@ -32,22 +34,10 @@ class ConfigAnalytics extends LitElement {
: undefined; : undefined;
return html` return html`
<ha-card <ha-card outlined>
outlined
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
"Home Assistant analytics"}
>
<div class="card-content"> <div class="card-content">
${error ? html`<div class="error">${error}</div>` : nothing} ${error ? html`<div class="error">${error}</div>` : ""}
<p> <p>${this.hass.localize("ui.panel.config.analytics.intro")}</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>
<ha-analytics <ha-analytics
translation_key_panel="config" translation_key_panel="config"
@analytics-preferences-changed=${this._preferencesChanged} @analytics-preferences-changed=${this._preferencesChanged}
@@ -55,59 +45,26 @@ class ConfigAnalytics extends LitElement {
.analytics=${this._analyticsDetails} .analytics=${this._analyticsDetails}
></ha-analytics> ></ha-analytics>
</div> </div>
</ha-card> <div class="card-actions">
${this._analyticsDetails && <ha-button @click=${this._save}>
"snapshots" in this._analyticsDetails.preferences ${this.hass.localize(
? html`<ha-card "ui.panel.config.core.section.core.core_config.save_button"
outlined
.header=${this.hass.localize(
"ui.panel.config.analytics.preferences.snapshots.header"
)} )}
> </ha-button>
<div class="card-content"> </div>
<p> </ha-card>
${this.hass.localize( <div class="footer">
"ui.panel.config.analytics.preferences.snapshots.info" <ha-button
)} size="small"
<a appearance="plain"
href=${documentationUrl(this.hass, "/device-database/")} href=${documentationUrl(this.hass, "/integrations/analytics/")}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
>${this.hass.localize( >
"ui.panel.config.analytics.preferences.snapshots.learn_more" <ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
)}</a ${this.hass.localize("ui.panel.config.analytics.learn_more")}
>. </ha-button>
</p> </div>
<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>
</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 { private _preferencesChanged(event: CustomEvent): void {
this._analyticsDetails = { this._analyticsDetails = {
...this._analyticsDetails!, ...this._analyticsDetails!,
preferences: event.detail.preferences, preferences: event.detail.preferences,
}; };
this._save();
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
@@ -174,10 +117,21 @@ class ConfigAnalytics extends LitElement {
p { p {
margin-top: 0; margin-top: 0;
} }
ha-card:not(:first-of-type) { .card-actions {
margin-top: 24px; 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

@@ -2,7 +2,9 @@ import { mdiDotsVertical, mdiDownload } from "@mdi/js";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import "../../../components/ha-button-menu";
import "../../../components/ha-icon-button"; import "../../../components/ha-icon-button";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { getSignedPath } from "../../../data/auth"; import { getSignedPath } from "../../../data/auth";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
@@ -12,8 +14,6 @@ import {
downloadFileSupported, downloadFileSupported,
fileDownload, fileDownload,
} from "../../../util/file_download"; } from "../../../util/file_download";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-dropdown";
@customElement("ha-config-section-analytics") @customElement("ha-config-section-analytics")
class HaConfigSectionAnalytics extends LitElement { class HaConfigSectionAnalytics extends LitElement {
@@ -33,19 +33,22 @@ class HaConfigSectionAnalytics extends LitElement {
> >
${downloadFileSupported(this.hass) ${downloadFileSupported(this.hass)
? html` ? html`
<ha-dropdown <ha-button-menu
@wa-select=${this._handleOverflowAction} @action=${this._handleOverflowAction}
slot="toolbar-icon" slot="toolbar-icon"
> >
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}> <ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
</ha-icon-button> </ha-icon-button>
<ha-dropdown-item .value=${"download_device_info"}> <ha-list-item graphic="icon">
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon> <ha-svg-icon
slot="graphic"
.path=${mdiDownload}
></ha-svg-icon>
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.analytics.download_device_info" "ui.panel.config.analytics.download_device_info"
)} )}
</ha-dropdown-item> </ha-list-item>
</ha-dropdown> </ha-button-menu>
` `
: nothing} : nothing}
<div class="content"> <div class="content">
@@ -55,16 +58,9 @@ class HaConfigSectionAnalytics extends LitElement {
`; `;
} }
private async _handleOverflowAction( private async _handleOverflowAction(): Promise<void> {
ev: CustomEvent<{ item: { value: string } }> const signedPath = await getSignedPath(this.hass, "/api/analytics/devices");
): Promise<void> { fileDownload(signedPath.path);
if (ev.detail.item.value === "download_device_info") {
const signedPath = await getSignedPath(
this.hass,
"/api/analytics/devices"
);
fileDownload(signedPath.path);
}
} }
static styles = css` static styles = css`

View File

@@ -1,3 +1,4 @@
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { mdiDotsVertical, mdiRefresh } from "@mdi/js"; import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
import type { HassEntities } from "home-assistant-js-websocket"; import type { HassEntities } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit"; import type { TemplateResult } from "lit";
@@ -5,9 +6,13 @@ import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-bar"; import "../../../components/ha-bar";
import "../../../components/ha-button-menu";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-check-list-item";
import "../../../components/ha-list-item";
import "../../../components/ha-metric"; import "../../../components/ha-metric";
import { extractApiErrorMessage } from "../../../data/hassio/common"; import { extractApiErrorMessage } from "../../../data/hassio/common";
import type { import type {
@@ -28,9 +33,6 @@ import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import "../dashboard/ha-config-updates"; import "../dashboard/ha-config-updates";
import { showJoinBetaDialog } from "./updates/show-dialog-join-beta"; import { showJoinBetaDialog } from "./updates/show-dialog-join-beta";
import "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "@home-assistant/webawesome/dist/components/divider/divider";
@customElement("ha-config-section-updates") @customElement("ha-config-section-updates")
class HaConfigSectionUpdates extends LitElement { class HaConfigSectionUpdates extends LitElement {
@@ -71,25 +73,24 @@ class HaConfigSectionUpdates extends LitElement {
.path=${mdiRefresh} .path=${mdiRefresh}
@click=${this._checkUpdates} @click=${this._checkUpdates}
></ha-icon-button> ></ha-icon-button>
<ha-dropdown @wa-select=${this._handleOverflowAction}> <ha-button-menu multi>
<ha-icon-button <ha-icon-button
slot="trigger" slot="trigger"
.label=${this.hass.localize("ui.common.menu")} .label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical} .path=${mdiDotsVertical}
></ha-icon-button> ></ha-icon-button>
<ha-check-list-item
<ha-dropdown-item left
type="checkbox" @request-selected=${this._toggleSkipped}
value="show_skipped" .selected=${this._showSkipped}
.checked=${this._showSkipped}
> >
${this.hass.localize("ui.panel.config.updates.show_skipped")} ${this.hass.localize("ui.panel.config.updates.show_skipped")}
</ha-dropdown-item> </ha-check-list-item>
${this._supervisorInfo ${this._supervisorInfo
? html` ? html`
<wa-divider></wa-divider> <li divider role="separator"></li>
<ha-dropdown-item <ha-list-item
value="toggle_beta" @request-selected=${this._toggleBeta}
.disabled=${this._supervisorInfo.channel === "dev"} .disabled=${this._supervisorInfo.channel === "dev"}
> >
${this._supervisorInfo.channel === "stable" ${this._supervisorInfo.channel === "stable"
@@ -97,10 +98,10 @@ class HaConfigSectionUpdates extends LitElement {
: this.hass.localize( : this.hass.localize(
"ui.panel.config.updates.leave_beta" "ui.panel.config.updates.leave_beta"
)} )}
</ha-dropdown-item> </ha-list-item>
` `
: ""} : ""}
</ha-dropdown> </ha-button-menu>
</div> </div>
<div class="content"> <div class="content">
<ha-card outlined> <ha-card outlined>
@@ -132,19 +133,27 @@ class HaConfigSectionUpdates extends LitElement {
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass); this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} }
private async _handleOverflowAction( private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
ev: CustomEvent<{ item: { value: string } }> if (ev.detail.source !== "property") {
return;
}
this._showSkipped = !this._showSkipped;
}
private async _toggleBeta(
ev: CustomEvent<RequestSelectedDetail>
): Promise<void> { ): Promise<void> {
if (ev.detail.item.value === "toggle_beta") { if (!shouldHandleRequestSelectedEvent(ev)) {
if (this._supervisorInfo!.channel === "stable") { return;
showJoinBetaDialog(this, { }
join: async () => this._setChannel("beta"),
}); if (this._supervisorInfo!.channel === "stable") {
} else { showJoinBetaDialog(this, {
this._setChannel("stable"); join: async () => this._setChannel("beta"),
} });
} else if (ev.detail.item.value === "show_skipped") { } else {
this._showSkipped = !this._showSkipped; this._setChannel("stable");
} }
} }

View File

@@ -1,9 +1,7 @@
import type { CSSResultGroup, TemplateResult } from "lit"; import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit"; import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name"; import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
import { stringCompare } from "../../../../common/string/compare";
import { titleCase } from "../../../../common/string/title-case"; import { titleCase } from "../../../../common/string/title-case";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import type { DeviceRegistryEntry } from "../../../../data/device_registry"; import type { DeviceRegistryEntry } from "../../../../data/device_registry";
@@ -11,61 +9,16 @@ import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { createSearchParam } from "../../../../common/url/search-params"; import { createSearchParam } from "../../../../common/url/search-params";
import { isComponentLoaded } from "../../../../common/config/is_component_loaded"; 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") @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 hass!: HomeAssistant;
@property({ attribute: false }) public device!: DeviceRegistryEntry; @property({ attribute: false }) public device!: DeviceRegistryEntry;
@property({ type: Boolean }) public narrow = false; @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 { protected render(): TemplateResult {
const { map: labelMap, ids: labels } = this._labelsData(
this._labelRegistry,
this.device.labels,
this.hass.locale.language
);
return html` return html`
<ha-card <ha-card
outlined outlined
@@ -105,7 +58,7 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
<span class="hub" <span class="hub"
><a ><a
href="/config/devices/device/${this.device.via_device_id}" href="/config/devices/device/${this.device.via_device_id}"
>${this._computeDeviceNameDisplay( >${this._computeDeviceNameDislay(
this.device.via_device_id this.device.via_device_id
)}</a )}</a
></span ></span
@@ -173,34 +126,6 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
</div> </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> <slot></slot>
</div> </div>
<slot name="actions"></slot> <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]; const device = this.hass.devices[deviceId];
return device return device
? computeDeviceNameDisplay(device, this.hass) ? computeDeviceNameDisplay(device, this.hass)
@@ -237,26 +162,8 @@ export class HaDeviceCard extends SubscribeMixin(LitElement) {
.device { .device {
width: 30%; 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 { .extra-info {
margin-top: var(--ha-space-2); margin-top: 8px;
word-wrap: break-word; word-wrap: break-word;
} }
.manuf, .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); 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")) { if (domains.includes("matter")) {
const matter = await import( const matter = await import(
"./device-detail/integration-elements/matter/device-actions" "./device-detail/integration-elements/matter/device-actions"

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

@@ -270,7 +270,6 @@ export class HaManualScriptEditor extends LitElement {
@value-changed=${this._sidebarConfigChanged} @value-changed=${this._sidebarConfigChanged}
@sidebar-resized=${this._resizeSidebar} @sidebar-resized=${this._resizeSidebar}
@sidebar-resizing-stopped=${this._stopResizeSidebar} @sidebar-resizing-stopped=${this._stopResizeSidebar}
@sidebar-reset-size=${this._resetSidebarWidth}
></ha-automation-sidebar> ></ha-automation-sidebar>
</div> </div>
</div> </div>
@@ -619,16 +618,6 @@ export class HaManualScriptEditor extends LitElement {
this._prevSidebarWidthPx = undefined; this._prevSidebarWidthPx = undefined;
} }
private _resetSidebarWidth(ev: Event) {
ev.stopPropagation();
this._prevSidebarWidthPx = undefined;
this._sidebarWidthPx = SIDEBAR_DEFAULT_WIDTH;
this.style.setProperty(
"--sidebar-dynamic-width",
`${this._sidebarWidthPx}px`
);
}
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return [ return [
saveFabStyles, saveFabStyles,

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import type { TemplateResult } from "lit"; 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 { customElement, property } from "lit/decorators";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
@@ -12,12 +12,6 @@ import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/ha-yaml-editor"; import "../../../../components/ha-yaml-editor";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { LocalizeKeys } from "../../../../common/translations/localize"; import type { LocalizeKeys } from "../../../../common/translations/localize";
import type {
ChatLogAssistantContent,
ChatLog,
ChatLogContent,
ChatLogUserContent,
} from "../../../../data/chat_log";
const RUN_DATA = ["pipeline", "language"]; const RUN_DATA = ["pipeline", "language"];
const WAKE_WORD_DATA = ["engine"]; const WAKE_WORD_DATA = ["engine"];
@@ -125,7 +119,7 @@ const dataMinusKeysRender = (
result[key] = data[key]; result[key] = data[key];
} }
return render return render
? html`<ha-expansion-panel class="yaml-expansion"> ? html`<ha-expansion-panel>
<span slot="header" <span slot="header"
>${hass.localize("ui.panel.config.voice_assistants.debug.raw")}</span >${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 pipelineRun!: PipelineRun;
@property({ attribute: false }) public chatLog?: ChatLog;
private _audioElement?: HTMLAudioElement; private _audioElement?: HTMLAudioElement;
private get _isPlaying(): boolean { private get _isPlaying(): boolean {
@@ -155,47 +147,31 @@ export class AssistPipelineDebug extends LitElement {
) || "ready" ) || "ready"
: "ready"; : "ready";
let messages: ChatLogContent[]; const messages: { from: string; text: string }[] = [];
if (this.chatLog) { const userMessage =
messages = this.chatLog.content.filter( (this.pipelineRun.init_options &&
this.pipelineRun.finished "text" in this.pipelineRun.init_options.input
? (content: ChatLogContent) => ? this.pipelineRun.init_options.input.text
content.role === "system" || : undefined) ||
(content.created >= this.pipelineRun.started && this.pipelineRun?.stt?.stt_output?.text ||
content.created <= this.pipelineRun.finished!) this.pipelineRun?.intent?.intent_input;
: (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. if (userMessage) {
const userMessage = messages.push({
(this.pipelineRun.init_options && from: "user",
"text" in this.pipelineRun.init_options.input text: userMessage,
? this.pipelineRun.init_options.input.text });
: undefined) || }
this.pipelineRun?.stt?.stt_output?.text ||
this.pipelineRun?.intent?.intent_input;
if (userMessage) { if (
messages.push({ this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
role: "user", ) {
content: userMessage, messages.push({
} as ChatLogUserContent); from: "hass",
} text: this.pipelineRun.intent.intent_output.response.speech.plain
.speech,
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);
}
} }
return html` return html`
@@ -214,58 +190,10 @@ export class AssistPipelineDebug extends LitElement {
${messages.length > 0 ${messages.length > 0
? html` ? html`
<div class="messages"> <div class="messages">
${messages.map((content) => ${messages.map(
content.role === "system" || content.role === "tool_result" ({ from, text }) => html`
? html` <div class=${`message ${from}`}>${text}</div>
<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}
`
)} )}
</div> </div>
<div style="clear:both"></div> <div style="clear:both"></div>
@@ -514,7 +442,7 @@ export class AssistPipelineDebug extends LitElement {
: ""} : ""}
${maybeRenderError(this.pipelineRun, "tts", lastRunStage)} ${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
<ha-card> <ha-card>
<ha-expansion-panel class="yaml-expansion"> <ha-expansion-panel>
<span slot="header" <span slot="header"
>${this.hass.localize( >${this.hass.localize(
"ui.panel.config.voice_assistants.debug.raw" "ui.panel.config.voice_assistants.debug.raw"
@@ -591,12 +519,12 @@ export class AssistPipelineDebug extends LitElement {
.row > div:last-child { .row > div:last-child {
text-align: right; text-align: right;
} }
.yaml-expansion { ha-expansion-panel {
padding-left: 8px; padding-left: 8px;
padding-inline-start: 8px; padding-inline-start: 8px;
padding-inline-end: initial; padding-inline-end: initial;
} }
.card-content .yaml-expansion { .card-content ha-expansion-panel {
padding-left: 0px; padding-left: 0px;
padding-inline-start: 0px; padding-inline-start: 0px;
padding-inline-end: initial; padding-inline-end: initial;
@@ -612,59 +540,27 @@ export class AssistPipelineDebug extends LitElement {
margin-top: 8px; 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 { .message {
padding: 8px;
}
.message,
.content-expansion {
font-size: var(--ha-font-size-l); font-size: var(--ha-font-size-l);
margin: 8px 0; margin: 8px 0;
padding: 8px;
border-radius: var(--ha-border-radius-xl); border-radius: var(--ha-border-radius-xl);
clear: both; clear: both;
} }
.messages pre { .message.user {
white-space: pre-wrap;
}
.user,
.tool_result {
margin-left: 24px; margin-left: 24px;
margin-inline-start: 24px; margin-inline-start: 24px;
margin-inline-end: initial; margin-inline-end: initial;
float: var(--float-end); float: var(--float-end);
text-align: right;
border-bottom-right-radius: 0px; border-bottom-right-radius: 0px;
background-color: var(--light-primary-color); background-color: var(--light-primary-color);
color: var(--text-light-primary-color, var(--primary-text-color)); color: var(--text-light-primary-color, var(--primary-text-color));
direction: var(--direction); direction: var(--direction);
} }
.message.user, .message.hass {
.content-expansion div[slot="header"] {
text-align: right;
}
.assistant {
margin-right: 24px; margin-right: 24px;
margin-inline-end: 24px; margin-inline-end: 24px;
margin-inline-start: initial; margin-inline-start: initial;

View File

@@ -4796,8 +4796,7 @@
"headers": { "headers": {
"name": "Name", "name": "Name",
"type": "Type", "type": "Type",
"file_name": "File name", "file_name": "File name"
"usage_count": "In use"
}, },
"types": { "types": {
"automation": "Automation", "automation": "Automation",
@@ -5412,11 +5411,6 @@
"partial_failure": "Some devices failed to delete successfully. Check system logs for more information." "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": { "entities": {
@@ -6785,7 +6779,6 @@
}, },
"analytics": { "analytics": {
"caption": "Analytics", "caption": "Analytics",
"header": "Home Assistant analytics",
"description": "Learn how to share data to improve Home Assistant", "description": "Learn how to share data to improve Home Assistant",
"preferences": { "preferences": {
"base": { "base": {
@@ -6803,21 +6796,10 @@
"diagnostics": { "diagnostics": {
"title": "Diagnostics", "title": "Diagnostics",
"description": "Share crash reports when unexpected errors occur." "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", "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.", "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" "download_device_info": "Preview device analytics"
}, },

View File

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