mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-26 19:27:27 +00:00
Compare commits
23 Commits
sec_pypi_p
...
unassigned
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5ce6419cd8 | ||
|
|
ff28ca11c9 | ||
|
|
82a3db39fe | ||
|
|
254857b53f | ||
|
|
1648be6b83 | ||
|
|
c4c774c217 | ||
|
|
6a0aab2088 | ||
|
|
5a5593ec5b | ||
|
|
2d39afdeac | ||
|
|
974ac31277 | ||
|
|
47e98d532d | ||
|
|
2efc513221 | ||
|
|
5d820e3046 | ||
|
|
c3cff3bcd3 | ||
|
|
e65a8a6b66 | ||
|
|
318452d6f6 | ||
|
|
6890823c31 | ||
|
|
ba9bab38c9 | ||
|
|
085b3884af | ||
|
|
a9c816ed9c | ||
|
|
c3201eecf3 | ||
|
|
bd67a3b90a | ||
|
|
e59ab45e60 |
4
.github/workflows/cast_deployment.yaml
vendored
4
.github/workflows/cast_deployment.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
8
.github/workflows/ci.yaml
vendored
8
.github/workflows/ci.yaml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -76,7 +76,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -100,7 +100,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
|
||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
# We must fetch at least the immediate parents so that if this is
|
||||
# a pull request then we can checkout the head.
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/init@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
|
||||
# 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)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/autobuild@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
@@ -57,4 +57,4 @@ jobs:
|
||||
# make release
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
|
||||
uses: github/codeql-action/analyze@e12f0178983d466f2f6028f5cc7a6d786fd97f4b # v4.31.4
|
||||
|
||||
4
.github/workflows/demo_deployment.yaml
vendored
4
.github/workflows/demo_deployment.yaml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: dev
|
||||
|
||||
@@ -57,7 +57,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
with:
|
||||
ref: master
|
||||
|
||||
|
||||
2
.github/workflows/design_deployment.yaml
vendored
2
.github/workflows/design_deployment.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
|
||||
2
.github/workflows/design_preview.yaml
vendored
2
.github/workflows/design_preview.yaml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
|
||||
steps:
|
||||
- name: Check out files from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
|
||||
2
.github/workflows/nightly.yaml
vendored
2
.github/workflows/nightly.yaml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6
|
||||
|
||||
6
.github/workflows/release.yaml
vendored
6
.github/workflows/release.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Set up Python ${{ env.PYTHON_VERSION }}
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
@@ -120,7 +120,7 @@ jobs:
|
||||
contents: write # Required to upload release assets
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/translations.yaml
vendored
2
.github/workflows/translations.yaml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout the repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
|
||||
|
||||
- name: Upload Translations
|
||||
run: |
|
||||
|
||||
@@ -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.5.0",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.18",
|
||||
"js-yaml": "4.1.1",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
import "../components/ha-button";
|
||||
@@ -118,6 +118,9 @@ export class HaAuthFlow extends LitElement {
|
||||
display: block;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.action ha-button {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
<form>${this._renderForm()}</form>
|
||||
`;
|
||||
|
||||
84
src/components/ha-condition-icon.ts
Normal file
84
src/components/ha-condition-icon.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
mdiAmpersand,
|
||||
mdiClockOutline,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiGateOr,
|
||||
mdiIdentifier,
|
||||
mdiMapMarkerRadius,
|
||||
mdiNotEqualVariant,
|
||||
mdiNumeric,
|
||||
mdiStateMachine,
|
||||
mdiWeatherSunny,
|
||||
} from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { conditionIcon, FALLBACK_DOMAIN_ICONS } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export const CONDITION_ICONS = {
|
||||
device: mdiDevices,
|
||||
and: mdiAmpersand,
|
||||
or: mdiGateOr,
|
||||
not: mdiNotEqualVariant,
|
||||
state: mdiStateMachine,
|
||||
numeric_state: mdiNumeric,
|
||||
sun: mdiWeatherSunny,
|
||||
template: mdiCodeBraces,
|
||||
time: mdiClockOutline,
|
||||
trigger: mdiIdentifier,
|
||||
zone: mdiMapMarkerRadius,
|
||||
};
|
||||
|
||||
@customElement("ha-condition-icon")
|
||||
export class HaConditionIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public condition?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
}
|
||||
|
||||
if (!this.condition) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = conditionIcon(this.hass, this.condition).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
const domain = computeDomain(this.condition!);
|
||||
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
.path=${CONDITION_ICONS[this.condition!] ||
|
||||
FALLBACK_DOMAIN_ICONS[domain]}
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-condition-icon": HaConditionIcon;
|
||||
}
|
||||
}
|
||||
@@ -75,11 +75,15 @@ 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(--secondary-text-color);
|
||||
color: var(
|
||||
--ha-dialog-header-subtitle-color,
|
||||
var(--secondary-text-color)
|
||||
);
|
||||
}
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
.header-bar {
|
||||
|
||||
@@ -209,6 +209,7 @@ export class HaExpansionPanel extends LitElement {
|
||||
::slotted([slot="header"]) {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
|
||||
@@ -66,7 +66,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
.path=${item.path}
|
||||
></ha-svg-icon>
|
||||
${item.label}
|
||||
</ha-md-menu-item> `
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
</ha-md-button-menu>`
|
||||
: html`
|
||||
@@ -103,6 +103,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
cursor: initial;
|
||||
}
|
||||
div[role="separator"] {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface DisplayItem {
|
||||
label: string;
|
||||
description?: string;
|
||||
disableSorting?: boolean;
|
||||
disableHiding?: boolean;
|
||||
}
|
||||
|
||||
export interface DisplayValue {
|
||||
@@ -101,6 +102,7 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
icon,
|
||||
iconPath,
|
||||
disableSorting,
|
||||
disableHiding,
|
||||
} = item;
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@@ -155,18 +157,21 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="end"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
||||
{
|
||||
label: label,
|
||||
}
|
||||
)}
|
||||
.value=${value}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
${!isVisible || !disableHiding
|
||||
? html`<ha-icon-button
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="end"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
||||
{
|
||||
label: label,
|
||||
}
|
||||
)}
|
||||
.value=${value}
|
||||
@click=${this._toggle}
|
||||
.disabled=${disableHiding || false}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${isVisible && !disableSorting
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
|
||||
@@ -36,6 +36,11 @@ export class HaMdMenuItem extends MenuItemEl {
|
||||
::slotted([slot="headline"]) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 1;
|
||||
--md-menu-item-label-text-color: var(--disabled-text-color);
|
||||
--md-menu-item-leading-icon-color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
||||
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
@@ -43,13 +43,8 @@ const createViewNavigationItem = (
|
||||
|
||||
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||
path: `/${panel.url_path}`,
|
||||
icon: panel.icon ?? "mdi:view-dashboard",
|
||||
title:
|
||||
panel.url_path === getDefaultPanelUrlPath(hass)
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title ||
|
||||
(panel.url_path ? titleCase(panel.url_path) : ""),
|
||||
icon: getPanelIcon(panel) || "mdi:view-dashboard",
|
||||
title: getPanelTitle(hass, panel) || "",
|
||||
});
|
||||
|
||||
@customElement("ha-navigation-picker")
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import {
|
||||
mdiBell,
|
||||
mdiCalendar,
|
||||
mdiCellphoneCog,
|
||||
mdiChartBox,
|
||||
mdiClipboardList,
|
||||
mdiCog,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiLightningBolt,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiTooltipAccount,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -33,7 +24,14 @@ import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import {
|
||||
FIXED_PANELS,
|
||||
getDefaultPanelUrlPath,
|
||||
getPanelIcon,
|
||||
getPanelIconPath,
|
||||
getPanelTitle,
|
||||
SHOW_AFTER_SPACER_PANELS,
|
||||
} from "../data/panel";
|
||||
import type { PersistentNotification } from "../data/persistent_notification";
|
||||
import { subscribeNotifications } from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
@@ -54,8 +52,6 @@ import "./ha-spinner";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
const SORT_VALUE_URL_PATHS = {
|
||||
@@ -67,18 +63,6 @@ const SORT_VALUE_URL_PATHS = {
|
||||
config: 11,
|
||||
};
|
||||
|
||||
export const PANEL_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
lovelace: mdiViewDashboard,
|
||||
map: mdiTooltipAccount,
|
||||
"media-browser": mdiPlayBoxMultiple,
|
||||
todo: mdiClipboardList,
|
||||
};
|
||||
|
||||
const panelSorter = (
|
||||
reverseSort: string[],
|
||||
defaultPanel: string,
|
||||
@@ -155,16 +139,23 @@ export const computePanels = memoizeOne(
|
||||
const beforeSpacer: PanelInfo[] = [];
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
const allPanels = Object.values(panels).filter(
|
||||
(panel) => !FIXED_PANELS.includes(panel.url_path)
|
||||
);
|
||||
|
||||
allPanels.forEach((panel) => {
|
||||
const isDefaultPanel = panel.url_path === defaultPanel;
|
||||
|
||||
if (
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(!panel.title && panel.url_path !== defaultPanel) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path))
|
||||
!isDefaultPanel &&
|
||||
(!panel.title ||
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path)))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
(SHOW_AFTER_SPACER_PANELS.includes(panel.url_path)
|
||||
? afterSpacer
|
||||
: beforeSpacer
|
||||
).push(panel);
|
||||
@@ -251,10 +242,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Show the supervisor as being part of configuration
|
||||
const selectedPanel = this.route.path?.startsWith("/hassio/")
|
||||
? "config"
|
||||
: this.hass.panelUrl;
|
||||
const selectedPanel = this.hass.panelUrl;
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
@@ -397,9 +385,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
private _renderAllPanels(selectedPanel: string) {
|
||||
if (!this._panelOrder || !this._hiddenPanels) {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}
|
||||
><ha-spinner size="small"></ha-spinner
|
||||
></ha-fade-in>
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="small"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -413,7 +401,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<ha-md-list
|
||||
class="ha-scrollbar"
|
||||
@@ -422,61 +409,42 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderExternalConfiguration()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this.hass.user?.is_admin
|
||||
? this._renderConfiguration(selectedPanel)
|
||||
: this._renderExternalConfiguration()}
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanels(
|
||||
panels: PanelInfo[],
|
||||
selectedPanel: string,
|
||||
defaultPanel: string
|
||||
) {
|
||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
panel.url_path === defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
: undefined,
|
||||
selectedPanel
|
||||
)
|
||||
this._renderPanel(panel, panel.url_path === selectedPanel)
|
||||
);
|
||||
}
|
||||
|
||||
private _renderPanel(
|
||||
urlPath: string,
|
||||
title: string | null,
|
||||
icon: string | null | undefined,
|
||||
iconPath: string | null | undefined,
|
||||
selectedPanel: string
|
||||
) {
|
||||
return urlPath === "config"
|
||||
? this._renderConfiguration(title, selectedPanel)
|
||||
: html`
|
||||
<ha-md-list-item
|
||||
.href=${`/${urlPath}`}
|
||||
type="link"
|
||||
class=${classMap({
|
||||
selected: selectedPanel === urlPath,
|
||||
})}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
private _renderPanel(panel: PanelInfo, isSelected: boolean) {
|
||||
const title = getPanelTitle(this.hass, panel);
|
||||
const urlPath = panel.url_path;
|
||||
const icon = getPanelIcon(panel);
|
||||
const iconPath = getPanelIconPath(panel);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
.href=${`/${urlPath}`}
|
||||
type="link"
|
||||
class=${classMap({ selected: isSelected })}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderDivider() {
|
||||
@@ -487,10 +455,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return html`<div class="spacer" disabled></div>`;
|
||||
}
|
||||
|
||||
private _renderConfiguration(title: string | null, selectedPanel: string) {
|
||||
private _renderConfiguration(selectedPanel: string) {
|
||||
if (!this.hass.user?.is_admin) {
|
||||
return nothing;
|
||||
}
|
||||
const isSelected =
|
||||
selectedPanel === "config" || this.route.path?.startsWith("/hassio/");
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
class="configuration${selectedPanel === "config" ? " selected" : ""}"
|
||||
class="configuration ${classMap({ selected: isSelected })}"
|
||||
type="button"
|
||||
href="/config"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -504,15 +477,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
${this._updatesCount + this._issuesCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("panel.config")}</span
|
||||
>
|
||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
@@ -535,19 +510,20 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
? html`
|
||||
<span class="badge" slot="start"> ${notificationCount} </span>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.notification_drawer.title")}</span
|
||||
>
|
||||
${this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderUserItem(selectedPanel: string) {
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isSelected = selectedPanel === "profile";
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@@ -555,7 +531,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
type="link"
|
||||
class=${classMap({
|
||||
user: true,
|
||||
selected: selectedPanel === "profile",
|
||||
selected: isSelected,
|
||||
rtl: isRTL,
|
||||
})}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -566,31 +542,30 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
.user=${this.hass.user}
|
||||
.hass=${this.hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.user ? this.hass.user.name : ""}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${!this.hass.user?.is_admin &&
|
||||
this.hass.auth.external?.config.hasSettingsScreen
|
||||
? html`
|
||||
<ha-md-list-item
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
type="button"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: ""}`;
|
||||
if (!this.hass.auth.external?.config.hasSettingsScreen) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
type="button"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleExternalAppConfiguration(ev: Event) {
|
||||
|
||||
178
src/components/ha-snowflakes.ts
Normal file
178
src/components/ha-snowflakes.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ export interface AnalyticsPreferences {
|
||||
diagnostics?: boolean;
|
||||
usage?: boolean;
|
||||
statistics?: boolean;
|
||||
snapshots?: boolean;
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
|
||||
@@ -214,6 +214,8 @@ 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"] &
|
||||
@@ -235,6 +237,7 @@ export const processEvent = (
|
||||
stage: "ready",
|
||||
run: event.data,
|
||||
events: [event],
|
||||
started: new Date(event.timestamp),
|
||||
};
|
||||
return run;
|
||||
}
|
||||
@@ -290,9 +293,14 @@ export const processEvent = (
|
||||
tts: { ...run.tts!, ...event.data, done: true },
|
||||
};
|
||||
} else if (event.type === "run-end") {
|
||||
run = { ...run, stage: "done" };
|
||||
run = { ...run, finished: new Date(event.timestamp), stage: "done" };
|
||||
} else if (event.type === "error") {
|
||||
run = { ...run, stage: "error", error: event.data };
|
||||
run = {
|
||||
...run,
|
||||
finished: new Date(event.timestamp),
|
||||
stage: "error",
|
||||
error: event.data,
|
||||
};
|
||||
} else {
|
||||
run = { ...run };
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import type { Context, HomeAssistant } from "../types";
|
||||
import type { BlueprintInput } from "./blueprint";
|
||||
import type { ConditionDescription } from "./condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import type { Action, Field, MODES } from "./script";
|
||||
@@ -236,6 +237,12 @@ interface BaseCondition {
|
||||
condition: string;
|
||||
alias?: string;
|
||||
enabled?: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PlatformCondition extends BaseCondition {
|
||||
condition: Exclude<string, LegacyCondition["condition"]>;
|
||||
target?: HassServiceTarget;
|
||||
}
|
||||
|
||||
export interface LogicalCondition extends BaseCondition {
|
||||
@@ -320,7 +327,7 @@ export type AutomationElementGroup = Record<
|
||||
{ icon?: string; members?: AutomationElementGroup }
|
||||
>;
|
||||
|
||||
export type Condition =
|
||||
export type LegacyCondition =
|
||||
| StateCondition
|
||||
| NumericStateCondition
|
||||
| SunCondition
|
||||
@@ -331,6 +338,8 @@ export type Condition =
|
||||
| LogicalCondition
|
||||
| TriggerCondition;
|
||||
|
||||
export type Condition = LegacyCondition | PlatformCondition;
|
||||
|
||||
export type ConditionWithShorthand =
|
||||
| Condition
|
||||
| ShorthandAndConditionList
|
||||
@@ -608,6 +617,7 @@ export interface ConditionSidebarConfig extends BaseSidebarConfig {
|
||||
insertAfter: (value: Condition | Condition[]) => boolean;
|
||||
toggleYamlMode: () => void;
|
||||
config: Condition;
|
||||
description?: ConditionDescription;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,14 @@ import {
|
||||
} from "../common/string/format-list";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { Condition, ForDict, LegacyTrigger, Trigger } from "./automation";
|
||||
import type {
|
||||
Condition,
|
||||
ForDict,
|
||||
LegacyCondition,
|
||||
LegacyTrigger,
|
||||
Trigger,
|
||||
} from "./automation";
|
||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import {
|
||||
localizeDeviceAutomationCondition,
|
||||
@@ -896,6 +903,39 @@ const tryDescribeCondition = (
|
||||
}
|
||||
}
|
||||
|
||||
const description = describeLegacyCondition(
|
||||
condition as LegacyCondition,
|
||||
hass,
|
||||
entityRegistry
|
||||
);
|
||||
|
||||
if (description) {
|
||||
return description;
|
||||
}
|
||||
|
||||
const conditionType = condition.condition;
|
||||
|
||||
const domain = getConditionDomain(condition.condition);
|
||||
const type = getConditionObjectId(condition.condition);
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${domain}.conditions.${type}.description_configured`
|
||||
) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${conditionType as LegacyCondition["condition"]}.label`
|
||||
) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.unknown_condition`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const describeLegacyCondition = (
|
||||
condition: LegacyCondition,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[]
|
||||
) => {
|
||||
if (condition.condition === "or") {
|
||||
const conditions = ensureArray(condition.conditions);
|
||||
|
||||
@@ -1287,12 +1327,5 @@ const tryDescribeCondition = (
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition.condition}.label`
|
||||
) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.unknown_condition`
|
||||
)
|
||||
);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
228
src/data/chat_log.ts
Normal file
228
src/data/chat_log.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
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",
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,38 +1,15 @@
|
||||
import {
|
||||
mdiAmpersand,
|
||||
mdiClockOutline,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiGateOr,
|
||||
mdiIdentifier,
|
||||
mdiMapClock,
|
||||
mdiMapMarkerRadius,
|
||||
mdiNotEqualVariant,
|
||||
mdiNumeric,
|
||||
mdiShape,
|
||||
mdiStateMachine,
|
||||
mdiWeatherSunny,
|
||||
} from "@mdi/js";
|
||||
import { mdiMapClock, mdiShape } from "@mdi/js";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AutomationElementGroupCollection } from "./automation";
|
||||
|
||||
export const CONDITION_ICONS = {
|
||||
device: mdiDevices,
|
||||
and: mdiAmpersand,
|
||||
or: mdiGateOr,
|
||||
not: mdiNotEqualVariant,
|
||||
state: mdiStateMachine,
|
||||
numeric_state: mdiNumeric,
|
||||
sun: mdiWeatherSunny,
|
||||
template: mdiCodeBraces,
|
||||
time: mdiClockOutline,
|
||||
trigger: mdiIdentifier,
|
||||
zone: mdiMapMarkerRadius,
|
||||
};
|
||||
import type { Selector, TargetSelector } from "./selector";
|
||||
|
||||
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
{
|
||||
groups: {
|
||||
device: {},
|
||||
dynamicGroups: {},
|
||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||
time_location: {
|
||||
icon: mdiMapClock,
|
||||
@@ -62,3 +39,33 @@ export const COLLAPSIBLE_CONDITION_ELEMENTS = [
|
||||
"ha-automation-condition-not",
|
||||
"ha-automation-condition-or",
|
||||
];
|
||||
|
||||
export interface ConditionDescription {
|
||||
target?: TargetSelector["target"];
|
||||
fields: Record<
|
||||
string,
|
||||
{
|
||||
example?: string | boolean | number;
|
||||
default?: unknown;
|
||||
required?: boolean;
|
||||
selector?: Selector;
|
||||
context?: Record<string, string>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export type ConditionDescriptions = Record<string, ConditionDescription>;
|
||||
|
||||
export const subscribeConditions = (
|
||||
hass: HomeAssistant,
|
||||
callback: (conditions: ConditionDescriptions) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<ConditionDescriptions>(callback, {
|
||||
type: "condition_platforms/subscribe",
|
||||
});
|
||||
|
||||
export const getConditionDomain = (condition: string) =>
|
||||
condition.includes(".") ? computeDomain(condition) : condition;
|
||||
|
||||
export const getConditionObjectId = (condition: string) =>
|
||||
condition.includes(".") ? computeObjectId(condition) : "_";
|
||||
|
||||
@@ -775,6 +775,7 @@ export const getEnergyDataCollection = (
|
||||
hass.locale,
|
||||
hass.config
|
||||
);
|
||||
collection.refresh();
|
||||
scheduleUpdatePeriod();
|
||||
},
|
||||
addHours(
|
||||
|
||||
@@ -7,8 +7,8 @@ export interface CoreFrontendUserData {
|
||||
}
|
||||
|
||||
export interface SidebarFrontendUserData {
|
||||
panelOrder: string[];
|
||||
hiddenPanels: string[];
|
||||
panelOrder?: string[];
|
||||
hiddenPanels?: string[];
|
||||
}
|
||||
|
||||
export interface CoreFrontendSystemData {
|
||||
|
||||
@@ -60,6 +60,7 @@ import type {
|
||||
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
|
||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||
|
||||
/** Icon to use when no icon specified for service. */
|
||||
export const DEFAULT_SERVICE_ICON = mdiRoomService;
|
||||
@@ -138,15 +139,25 @@ const resources: {
|
||||
all?: Promise<Record<string, TriggerIcons>>;
|
||||
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
|
||||
};
|
||||
conditions: {
|
||||
all?: Promise<Record<string, ConditionIcons>>;
|
||||
domains: Record<string, ConditionIcons | Promise<ConditionIcons>>;
|
||||
};
|
||||
} = {
|
||||
entity: {},
|
||||
entity_component: {},
|
||||
services: { domains: {} },
|
||||
triggers: { domains: {} },
|
||||
conditions: { domains: {} },
|
||||
};
|
||||
|
||||
interface IconResources<
|
||||
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
|
||||
T extends
|
||||
| ComponentIcons
|
||||
| PlatformIcons
|
||||
| ServiceIcons
|
||||
| TriggerIcons
|
||||
| ConditionIcons,
|
||||
> {
|
||||
resources: Record<string, T>;
|
||||
}
|
||||
@@ -195,17 +206,24 @@ type TriggerIcons = Record<
|
||||
{ trigger: string; sections?: Record<string, string> }
|
||||
>;
|
||||
|
||||
type ConditionIcons = Record<
|
||||
string,
|
||||
{ condition: string; sections?: Record<string, string> }
|
||||
>;
|
||||
|
||||
export type IconCategory =
|
||||
| "entity"
|
||||
| "entity_component"
|
||||
| "services"
|
||||
| "triggers";
|
||||
| "triggers"
|
||||
| "conditions";
|
||||
|
||||
interface CategoryType {
|
||||
entity: PlatformIcons;
|
||||
entity_component: ComponentIcons;
|
||||
services: ServiceIcons;
|
||||
triggers: TriggerIcons;
|
||||
conditions: ConditionIcons;
|
||||
}
|
||||
|
||||
export const getHassIcons = async <T extends IconCategory>(
|
||||
@@ -327,6 +345,13 @@ export const getTriggerIcons = async (
|
||||
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> =>
|
||||
getCategoryIcons(hass, "triggers", domain, force);
|
||||
|
||||
export const getConditionIcons = async (
|
||||
hass: HomeAssistant,
|
||||
domain?: string,
|
||||
force = false
|
||||
): Promise<ConditionIcons | Record<string, ConditionIcons> | undefined> =>
|
||||
getCategoryIcons(hass, "conditions", domain, force);
|
||||
|
||||
// Cache for sorted range keys
|
||||
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
|
||||
|
||||
@@ -526,6 +551,25 @@ export const triggerIcon = async (
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const conditionIcon = async (
|
||||
hass: HomeAssistant,
|
||||
condition: string
|
||||
): Promise<string | undefined> => {
|
||||
let icon: string | undefined;
|
||||
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionIcons = await getConditionIcons(hass, domain);
|
||||
if (conditionIcons) {
|
||||
const conditionName = getConditionObjectId(condition);
|
||||
const condIcon = conditionIcons[conditionName] as ConditionIcons[string];
|
||||
icon = condIcon?.condition;
|
||||
}
|
||||
if (!icon) {
|
||||
icon = await domainIcon(hass, domain);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const serviceIcon = async (
|
||||
hass: HomeAssistant,
|
||||
service: string
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiCalendar,
|
||||
mdiChartBox,
|
||||
mdiClipboardList,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiLightningBolt,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiTooltipAccount,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
@@ -60,7 +72,7 @@ export const getPanelTitleFromUrlPath = (
|
||||
return getPanelTitle(hass, panel);
|
||||
};
|
||||
|
||||
export const getPanelIcon = (panel: PanelInfo): string | null => {
|
||||
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
|
||||
if (!panel.icon) {
|
||||
switch (panel.component_name) {
|
||||
case "profile":
|
||||
@@ -70,5 +82,24 @@ export const getPanelIcon = (panel: PanelInfo): string | null => {
|
||||
}
|
||||
}
|
||||
|
||||
return panel.icon;
|
||||
return panel.icon || undefined;
|
||||
};
|
||||
|
||||
export const PANEL_ICON_PATHS = {
|
||||
calendar: mdiCalendar,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
lovelace: mdiViewDashboard,
|
||||
profile: mdiAccount,
|
||||
map: mdiTooltipAccount,
|
||||
"media-browser": mdiPlayBoxMultiple,
|
||||
todo: mdiClipboardList,
|
||||
};
|
||||
|
||||
export const getPanelIconPath = (panel: PanelInfo): string | undefined =>
|
||||
PANEL_ICON_PATHS[panel.url_path];
|
||||
|
||||
export const FIXED_PANELS = ["profile", "config"];
|
||||
export const SHOW_AFTER_SPACER_PANELS = ["developer-tools"];
|
||||
|
||||
@@ -75,7 +75,8 @@ export type TranslationCategory =
|
||||
| "preview_features"
|
||||
| "selector"
|
||||
| "services"
|
||||
| "triggers";
|
||||
| "triggers"
|
||||
| "conditions";
|
||||
|
||||
export const subscribeTranslationPreferences = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -2,14 +2,15 @@ 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-list-item";
|
||||
import "../../components/ha-md-list-item";
|
||||
import "../../components/ha-spinner";
|
||||
import type {
|
||||
ExternalEntityAddToActions,
|
||||
ExternalEntityAddToAction,
|
||||
ExternalEntityAddToActions,
|
||||
} from "../../external_app/external_messaging";
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-more-info-add-to")
|
||||
@@ -51,6 +52,7 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
app_payload: action.app_payload,
|
||||
},
|
||||
});
|
||||
fireEvent(this, "add-to-action-selected");
|
||||
} catch (err: any) {
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
@@ -91,19 +93,18 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
<div class="actions-list">
|
||||
${this._externalActions.actions.map(
|
||||
(action) => html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
.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="secondary">${action.details}</span>`
|
||||
? html`<span slot="supporting-text">${action.details}</span>`
|
||||
: nothing}
|
||||
<ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
|
||||
</ha-list-item>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -129,15 +130,6 @@ 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;
|
||||
@@ -149,4 +141,8 @@ declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-more-info-add-to": HaMoreInfoAddTo;
|
||||
}
|
||||
|
||||
interface HASSDomEvents {
|
||||
"add-to-action-selected": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,6 +645,7 @@ export class MoreInfoDialog extends LitElement {
|
||||
<ha-more-info-add-to
|
||||
.hass=${this.hass}
|
||||
.entityId=${entityId}
|
||||
@add-to-action-selected=${this._goBack}
|
||||
></ha-more-info-add-to>
|
||||
`
|
||||
: nothing
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { mdiClose, mdiDotsVertical, mdiRestart } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -9,18 +9,30 @@ import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-fade-in";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-items-display-editor";
|
||||
import type { DisplayValue } from "../../components/ha-items-display-editor";
|
||||
import type {
|
||||
DisplayItem,
|
||||
DisplayValue,
|
||||
} from "../../components/ha-items-display-editor";
|
||||
import "../../components/ha-md-button-menu";
|
||||
import "../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../components/ha-md-dialog";
|
||||
import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar";
|
||||
import "../../components/ha-md-menu-item";
|
||||
import { computePanels } from "../../components/ha-sidebar";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-svg-icon";
|
||||
import {
|
||||
fetchFrontendUserData,
|
||||
saveFrontendUserData,
|
||||
} from "../../data/frontend";
|
||||
import {
|
||||
getDefaultPanelUrlPath,
|
||||
getPanelIcon,
|
||||
getPanelIconPath,
|
||||
getPanelTitle,
|
||||
SHOW_AFTER_SPACER_PANELS,
|
||||
} from "../../data/panel";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { getDefaultPanelUrlPath } from "../../data/panel";
|
||||
|
||||
@customElement("dialog-edit-sidebar")
|
||||
class DialogEditSidebar extends LitElement {
|
||||
@@ -105,48 +117,53 @@ class DialogEditSidebar extends LitElement {
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
// Add default hidden panels that are missing in hidden
|
||||
const orderSet = new Set(this._order);
|
||||
const hiddenSet = new Set(this._hidden);
|
||||
|
||||
for (const panel of panels) {
|
||||
if (
|
||||
panel.default_visible === false &&
|
||||
!this._order.includes(panel.url_path) &&
|
||||
!this._hidden.includes(panel.url_path)
|
||||
!orderSet.has(panel.url_path) &&
|
||||
!hiddenSet.has(panel.url_path)
|
||||
) {
|
||||
this._hidden.push(panel.url_path);
|
||||
hiddenSet.add(panel.url_path);
|
||||
}
|
||||
}
|
||||
|
||||
if (hiddenSet.has(defaultPanel)) {
|
||||
hiddenSet.delete(defaultPanel);
|
||||
}
|
||||
|
||||
const hiddenPanels = Array.from(hiddenSet);
|
||||
|
||||
const items = [
|
||||
...beforeSpacer,
|
||||
...panels.filter((panel) => this._hidden!.includes(panel.url_path)),
|
||||
...afterSpacer.filter((panel) => panel.url_path !== "config"),
|
||||
].map((panel) => ({
|
||||
...panels.filter((panel) => hiddenPanels.includes(panel.url_path)),
|
||||
...afterSpacer,
|
||||
].map<DisplayItem>((panel) => ({
|
||||
value: panel.url_path,
|
||||
label:
|
||||
panel.url_path === defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
|
||||
icon: panel.icon || undefined,
|
||||
iconPath:
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
: undefined,
|
||||
disableSorting: panel.url_path === "developer-tools",
|
||||
(getPanelTitle(this.hass, panel) || panel.url_path) +
|
||||
`${defaultPanel === panel.url_path ? " (default)" : ""}`,
|
||||
icon: getPanelIcon(panel),
|
||||
iconPath: getPanelIconPath(panel),
|
||||
disableSorting: SHOW_AFTER_SPACER_PANELS.includes(panel.url_path),
|
||||
disableHiding: panel.url_path === defaultPanel,
|
||||
}));
|
||||
|
||||
return html`<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.value=${{
|
||||
order: this._order,
|
||||
hidden: this._hidden,
|
||||
}}
|
||||
.items=${items}
|
||||
@value-changed=${this._changed}
|
||||
dont-sort-visible
|
||||
>
|
||||
</ha-items-display-editor>`;
|
||||
return html`
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.value=${{
|
||||
order: this._order,
|
||||
hidden: hiddenPanels,
|
||||
}}
|
||||
.items=${items}
|
||||
@value-changed=${this._changed}
|
||||
dont-sort-visible
|
||||
>
|
||||
</ha-items-display-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -171,6 +188,22 @@ class DialogEditSidebar extends LitElement {
|
||||
>${this.hass.localize("ui.sidebar.edit_subtitle")}</span
|
||||
>`
|
||||
: nothing}
|
||||
<ha-md-button-menu
|
||||
slot="actionItems"
|
||||
positioning="popover"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item .clickAction=${this._resetToDefaults}>
|
||||
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
|
||||
${this.hass.localize("ui.sidebar.reset_to_defaults")}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-dialog-header>
|
||||
<div slot="content" class="content">${this._renderContent()}</div>
|
||||
<div slot="actions">
|
||||
@@ -194,6 +227,26 @@ class DialogEditSidebar extends LitElement {
|
||||
this._hidden = [...hidden];
|
||||
}
|
||||
|
||||
private _resetToDefaults = async () => {
|
||||
const confirmation = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize("ui.sidebar.reset_confirmation"),
|
||||
confirmText: this.hass.localize("ui.common.reset"),
|
||||
});
|
||||
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._order = [];
|
||||
this._hidden = [];
|
||||
try {
|
||||
await saveFrontendUserData(this.hass.connection, "sidebar", {});
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
}
|
||||
this.closeDialog();
|
||||
};
|
||||
|
||||
private async _save() {
|
||||
if (this._migrateToUserData) {
|
||||
const confirmation = await showConfirmationDialog(this, {
|
||||
|
||||
@@ -7,6 +7,7 @@ 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";
|
||||
@@ -50,6 +51,7 @@ 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}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../../common/string/compare";
|
||||
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../../../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../../components/ha-select";
|
||||
import type { Condition } from "../../../../../data/automation";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type Condition,
|
||||
} from "../../../../../data/automation";
|
||||
import type { ConditionDescriptions } from "../../../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
CONDITION_ICONS,
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
subscribeConditions,
|
||||
} from "../../../../../data/condition";
|
||||
import type { Entries, HomeAssistant } from "../../../../../types";
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../../condition/ha-automation-condition-editor";
|
||||
import "../../condition/types/ha-automation-condition-and";
|
||||
@@ -30,7 +40,10 @@ import "../../condition/types/ha-automation-condition-zone";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-condition")
|
||||
export class HaConditionAction extends LitElement implements ActionElement {
|
||||
export class HaConditionAction
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements ActionElement
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -43,6 +56,8 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "indent" }) public indent = false;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@query("ha-automation-condition-editor")
|
||||
private _conditionEditor?: HaAutomationConditionEditor;
|
||||
|
||||
@@ -50,6 +65,21 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
return { condition: "state" };
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeConditions(this.hass, (conditions) =>
|
||||
this._addConditions(conditions)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _addConditions(conditions: ConditionDescriptions) {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
|
||||
this.action.condition
|
||||
@@ -64,19 +94,25 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
"ui.panel.config.automation.editor.conditions.type_select"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.action.condition}
|
||||
.value=${this.action.condition in this._conditionDescriptions
|
||||
? `${DYNAMIC_PREFIX}${this.action.condition}`
|
||||
: this.action.condition}
|
||||
naturalMenuWidth
|
||||
@selected=${this._typeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._processedTypes(this.hass.localize).map(
|
||||
([opt, label, icon]) => html`
|
||||
${this._processedTypes(
|
||||
this._conditionDescriptions,
|
||||
this.hass.localize
|
||||
).map(
|
||||
([opt, label, condition]) => html`
|
||||
<ha-list-item .value=${opt} graphic="icon">
|
||||
${label}<ha-svg-icon
|
||||
${label}
|
||||
<ha-condition-icon
|
||||
slot="graphic"
|
||||
.path=${icon}
|
||||
></ha-svg-icon
|
||||
></ha-list-item>
|
||||
.condition=${condition}
|
||||
></ha-condition-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
@@ -88,11 +124,14 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
? html`
|
||||
<ha-automation-condition-editor
|
||||
.condition=${this.action}
|
||||
.description=${this._conditionDescriptions[this.action.condition]}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.narrow=${this.narrow}
|
||||
.uiSupported=${this._uiSupported(this.action.condition)}
|
||||
.uiSupported=${this._uiSupported(
|
||||
this._getType(this.action, this._conditionDescriptions)
|
||||
)}
|
||||
.indent=${this.indent}
|
||||
action
|
||||
></ha-automation-condition-editor>
|
||||
@@ -102,19 +141,46 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
private _processedTypes = memoizeOne(
|
||||
(localize: LocalizeFunc): [string, string, string][] =>
|
||||
(Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
|
||||
.map(
|
||||
([condition, icon]) =>
|
||||
[
|
||||
condition,
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
|
||||
),
|
||||
icon,
|
||||
] as [string, string, string]
|
||||
)
|
||||
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
|
||||
(
|
||||
conditionDescriptions: ConditionDescriptions,
|
||||
localize: LocalizeFunc
|
||||
): [string, string, string][] => {
|
||||
const legacy = (
|
||||
Object.keys(CONDITION_ICONS) as (keyof typeof CONDITION_ICONS)[]
|
||||
).map(
|
||||
(condition) =>
|
||||
[
|
||||
condition,
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
|
||||
),
|
||||
condition,
|
||||
] as [string, string, string]
|
||||
);
|
||||
const platform = Object.keys(conditionDescriptions).map((condition) => {
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionObjId = getConditionObjectId(condition);
|
||||
return [
|
||||
`${DYNAMIC_PREFIX}${condition}`,
|
||||
localize(`component.${domain}.conditions.${conditionObjId}.name`) ||
|
||||
condition,
|
||||
condition,
|
||||
] as [string, string, string];
|
||||
});
|
||||
return [...legacy, ...platform].sort((a, b) =>
|
||||
stringCompare(a[1], b[1], this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _getType = memoizeOne(
|
||||
(condition: Condition, conditionDescriptions: ConditionDescriptions) => {
|
||||
if (condition.condition in conditionDescriptions) {
|
||||
return "platform";
|
||||
}
|
||||
|
||||
return condition.condition;
|
||||
}
|
||||
);
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
@@ -132,6 +198,18 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDynamic(type)) {
|
||||
const value = getValueFromDynamic(type);
|
||||
if (value !== this.action.condition) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
condition: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const elClass = customElements.get(
|
||||
`ha-automation-condition-${type}`
|
||||
) as CustomElementConstructor & {
|
||||
|
||||
@@ -56,12 +56,19 @@ import {
|
||||
type AutomationElementGroup,
|
||||
type AutomationElementGroupCollection,
|
||||
} from "../../../data/automation";
|
||||
import type { ConditionDescriptions } from "../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS_GROUP,
|
||||
CONDITION_COLLECTIONS,
|
||||
CONDITION_ICONS,
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
subscribeConditions,
|
||||
} from "../../../data/condition";
|
||||
import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
|
||||
import {
|
||||
getConditionIcons,
|
||||
getServiceIcons,
|
||||
getTriggerIcons,
|
||||
} from "../../../data/icons";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import {
|
||||
domainToName,
|
||||
@@ -82,6 +89,7 @@ import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
||||
import { CONDITION_ICONS } from "../../../components/ha-condition-icon";
|
||||
|
||||
const TYPES = {
|
||||
trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS },
|
||||
@@ -119,7 +127,7 @@ const ENTITY_DOMAINS_OTHER = new Set([
|
||||
|
||||
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
|
||||
|
||||
const ACTION_SERVICE_KEYWORDS = ["dynamicGroups", "helpers", "other"];
|
||||
const DYNAMIC_KEYWORDS = ["dynamicGroups", "helpers", "other"];
|
||||
|
||||
@customElement("add-automation-element-dialog")
|
||||
class DialogAddAutomationElement
|
||||
@@ -152,6 +160,8 @@ class DialogAddAutomationElement
|
||||
|
||||
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@query(".items ha-md-list ha-md-list-item")
|
||||
private _itemsListFirstElement?: HaMdList;
|
||||
|
||||
@@ -169,15 +179,15 @@ class DialogAddAutomationElement
|
||||
|
||||
this.addKeyboardShortcuts();
|
||||
|
||||
this._unsubscribe();
|
||||
this._fetchManifests();
|
||||
|
||||
if (this._params?.type === "action") {
|
||||
this.hass.loadBackendTranslation("services");
|
||||
this._fetchManifests();
|
||||
this._calculateUsedDomains();
|
||||
getServiceIcons(this.hass);
|
||||
}
|
||||
if (this._params?.type === "trigger") {
|
||||
} else if (this._params?.type === "trigger") {
|
||||
this.hass.loadBackendTranslation("triggers");
|
||||
this._fetchManifests();
|
||||
getTriggerIcons(this.hass);
|
||||
this._unsub = subscribeTriggers(this.hass, (triggers) => {
|
||||
this._triggerDescriptions = {
|
||||
@@ -185,7 +195,17 @@ class DialogAddAutomationElement
|
||||
...triggers,
|
||||
};
|
||||
});
|
||||
} else if (this._params?.type === "condition") {
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
getConditionIcons(this.hass);
|
||||
this._unsub = subscribeConditions(this.hass, (conditions) => {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
this._fullScreen = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
@@ -199,10 +219,7 @@ class DialogAddAutomationElement
|
||||
|
||||
public closeDialog() {
|
||||
this.removeKeyboardShortcuts();
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
this._unsubscribe();
|
||||
if (this._params) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -219,6 +236,13 @@ class DialogAddAutomationElement
|
||||
return true;
|
||||
}
|
||||
|
||||
private _unsubscribe() {
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _getGroups = (
|
||||
type: AddAutomationElementDialogParams["type"],
|
||||
group?: string,
|
||||
@@ -348,8 +372,11 @@ class DialogAddAutomationElement
|
||||
items.push(
|
||||
...this._triggers(localize, this._triggerDescriptions, manifests)
|
||||
);
|
||||
}
|
||||
if (type === "action") {
|
||||
} else if (type === "condition") {
|
||||
items.push(
|
||||
...this._conditions(localize, this._conditionDescriptions, manifests)
|
||||
);
|
||||
} else if (type === "action") {
|
||||
items.push(...this._services(localize, services, manifests));
|
||||
}
|
||||
return items;
|
||||
@@ -372,6 +399,7 @@ class DialogAddAutomationElement
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
triggerDescriptions: TriggerDescriptions,
|
||||
conditionDescriptions: ConditionDescriptions,
|
||||
manifests?: DomainManifestLookup
|
||||
): {
|
||||
titleKey?: LocalizeKeys;
|
||||
@@ -383,35 +411,10 @@ class DialogAddAutomationElement
|
||||
let collectionGroups = Object.entries(collection.groups);
|
||||
const groups: ListItem[] = [];
|
||||
|
||||
if (
|
||||
type === "action" &&
|
||||
Object.keys(collection.groups).some((item) =>
|
||||
ACTION_SERVICE_KEYWORDS.includes(item)
|
||||
)
|
||||
) {
|
||||
groups.push(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
collection.groups.dynamicGroups
|
||||
? undefined
|
||||
: collection.groups.helpers
|
||||
? "helper"
|
||||
: "other"
|
||||
)
|
||||
);
|
||||
|
||||
collectionGroups = collectionGroups.filter(
|
||||
([key]) => !ACTION_SERVICE_KEYWORDS.includes(key)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
type === "trigger" &&
|
||||
Object.keys(collection.groups).some((item) =>
|
||||
ACTION_SERVICE_KEYWORDS.includes(item)
|
||||
DYNAMIC_KEYWORDS.includes(item)
|
||||
)
|
||||
) {
|
||||
groups.push(
|
||||
@@ -429,7 +432,53 @@ class DialogAddAutomationElement
|
||||
);
|
||||
|
||||
collectionGroups = collectionGroups.filter(
|
||||
([key]) => !ACTION_SERVICE_KEYWORDS.includes(key)
|
||||
([key]) => !DYNAMIC_KEYWORDS.includes(key)
|
||||
);
|
||||
} else if (
|
||||
type === "condition" &&
|
||||
Object.keys(collection.groups).some((item) =>
|
||||
DYNAMIC_KEYWORDS.includes(item)
|
||||
)
|
||||
) {
|
||||
groups.push(
|
||||
...this._conditionGroups(
|
||||
localize,
|
||||
conditionDescriptions,
|
||||
manifests,
|
||||
domains,
|
||||
collection.groups.dynamicGroups
|
||||
? undefined
|
||||
: collection.groups.helpers
|
||||
? "helper"
|
||||
: "other"
|
||||
)
|
||||
);
|
||||
|
||||
collectionGroups = collectionGroups.filter(
|
||||
([key]) => !DYNAMIC_KEYWORDS.includes(key)
|
||||
);
|
||||
} else if (
|
||||
type === "action" &&
|
||||
Object.keys(collection.groups).some((item) =>
|
||||
DYNAMIC_KEYWORDS.includes(item)
|
||||
)
|
||||
) {
|
||||
groups.push(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
collection.groups.dynamicGroups
|
||||
? undefined
|
||||
: collection.groups.helpers
|
||||
? "helper"
|
||||
: "other"
|
||||
)
|
||||
);
|
||||
|
||||
collectionGroups = collectionGroups.filter(
|
||||
([key]) => !DYNAMIC_KEYWORDS.includes(key)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -487,10 +536,6 @@ class DialogAddAutomationElement
|
||||
services: HomeAssistant["services"],
|
||||
manifests?: DomainManifestLookup
|
||||
): ListItem[] => {
|
||||
if (type === "action" && isDynamic(group)) {
|
||||
return this._services(localize, services, manifests, group);
|
||||
}
|
||||
|
||||
if (type === "trigger" && isDynamic(group)) {
|
||||
return this._triggers(
|
||||
localize,
|
||||
@@ -499,6 +544,17 @@ class DialogAddAutomationElement
|
||||
group
|
||||
);
|
||||
}
|
||||
if (type === "condition" && isDynamic(group)) {
|
||||
return this._conditions(
|
||||
localize,
|
||||
this._conditionDescriptions,
|
||||
manifests,
|
||||
group
|
||||
);
|
||||
}
|
||||
if (type === "action" && isDynamic(group)) {
|
||||
return this._services(localize, services, manifests, group);
|
||||
}
|
||||
|
||||
const groups = this._getGroups(type, group, collectionIndex);
|
||||
|
||||
@@ -688,6 +744,102 @@ class DialogAddAutomationElement
|
||||
}
|
||||
);
|
||||
|
||||
private _conditionGroups = (
|
||||
localize: LocalizeFunc,
|
||||
conditions: ConditionDescriptions,
|
||||
manifests: DomainManifestLookup | undefined,
|
||||
domains: Set<string> | undefined,
|
||||
type: "helper" | "other" | undefined
|
||||
): ListItem[] => {
|
||||
if (!conditions || !manifests) {
|
||||
return [];
|
||||
}
|
||||
const result: ListItem[] = [];
|
||||
const addedDomains = new Set<string>();
|
||||
Object.keys(conditions).forEach((condition) => {
|
||||
const domain = getConditionDomain(condition);
|
||||
|
||||
if (addedDomains.has(domain)) {
|
||||
return;
|
||||
}
|
||||
addedDomains.add(domain);
|
||||
|
||||
const manifest = manifests[domain];
|
||||
const domainUsed = !domains ? true : domains.has(domain);
|
||||
|
||||
if (
|
||||
(type === undefined &&
|
||||
(ENTITY_DOMAINS_MAIN.has(domain) ||
|
||||
(manifest?.integration_type === "entity" &&
|
||||
domainUsed &&
|
||||
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
|
||||
(type === "helper" && manifest?.integration_type === "helper") ||
|
||||
(type === "other" &&
|
||||
!ENTITY_DOMAINS_MAIN.has(domain) &&
|
||||
(ENTITY_DOMAINS_OTHER.has(domain) ||
|
||||
(!domainUsed && manifest?.integration_type === "entity") ||
|
||||
!["helper", "entity"].includes(manifest?.integration_type || "")))
|
||||
) {
|
||||
result.push({
|
||||
icon: html`
|
||||
<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
`,
|
||||
key: `${DYNAMIC_PREFIX}${domain}`,
|
||||
name: domainToName(localize, domain, manifest),
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
return result.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
};
|
||||
|
||||
private _conditions = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
conditions: ConditionDescriptions,
|
||||
_manifests: DomainManifestLookup | undefined,
|
||||
group?: string
|
||||
): ListItem[] => {
|
||||
if (!conditions) {
|
||||
return [];
|
||||
}
|
||||
const result: ListItem[] = [];
|
||||
|
||||
for (const condition of Object.keys(conditions)) {
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionName = getConditionObjectId(condition);
|
||||
|
||||
if (group && group !== `${DYNAMIC_PREFIX}${domain}`) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
icon: html`
|
||||
<ha-condition-icon
|
||||
.hass=${this.hass}
|
||||
.condition=${condition}
|
||||
></ha-condition-icon>
|
||||
`,
|
||||
key: `${DYNAMIC_PREFIX}${condition}`,
|
||||
name:
|
||||
localize(`component.${domain}.conditions.${conditionName}.name`) ||
|
||||
condition,
|
||||
description:
|
||||
localize(
|
||||
`component.${domain}.conditions.${conditionName}.description`
|
||||
) || condition,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
private _services = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
@@ -832,6 +984,7 @@ class DialogAddAutomationElement
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._triggerDescriptions,
|
||||
this._conditionDescriptions,
|
||||
this._manifests
|
||||
);
|
||||
|
||||
@@ -1136,6 +1289,7 @@ class DialogAddAutomationElement
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("resize", this._updateNarrow);
|
||||
this._removeSearchKeybindings();
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
private _updateNarrow = () => {
|
||||
|
||||
@@ -8,11 +8,13 @@ import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { Condition } from "../../../../data/automation";
|
||||
import { expandConditionWithShorthand } from "../../../../data/automation";
|
||||
import type { ConditionDescription } from "../../../../data/condition";
|
||||
import { COLLAPSIBLE_CONDITION_ELEMENTS } from "../../../../data/condition";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { editorStyles, indentStyle } from "../styles";
|
||||
import type { ConditionElement } from "./ha-automation-condition-row";
|
||||
import "./types/ha-automation-condition-platform";
|
||||
|
||||
@customElement("ha-automation-condition-editor")
|
||||
export default class HaAutomationConditionEditor extends LitElement {
|
||||
@@ -35,6 +37,8 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
|
||||
false;
|
||||
|
||||
@property({ attribute: false }) public description?: ConditionDescription;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
|
||||
@query(COLLAPSIBLE_CONDITION_ELEMENTS.join(", "))
|
||||
@@ -83,16 +87,23 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${dynamicElement(
|
||||
`ha-automation-condition-${condition.condition}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
condition: condition,
|
||||
disabled: this.disabled,
|
||||
optionsInSidebar: this.indent,
|
||||
narrow: this.narrow,
|
||||
}
|
||||
)}
|
||||
${this.description
|
||||
? html`<ha-automation-condition-platform
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.description=${this.description}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-condition-platform>`
|
||||
: dynamicElement(
|
||||
`ha-automation-condition-${condition.condition}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
condition: condition,
|
||||
disabled: this.disabled,
|
||||
optionsInSidebar: this.indent,
|
||||
narrow: this.narrow,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-automation-row";
|
||||
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-condition-icon";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
@@ -44,10 +45,8 @@ import type {
|
||||
} from "../../../../data/automation";
|
||||
import { isCondition, testCondition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
CONDITION_ICONS,
|
||||
} from "../../../../data/condition";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
@@ -130,6 +129,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" })
|
||||
public optionsInSidebar = false;
|
||||
|
||||
@@ -179,11 +181,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
private _renderRow() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
<ha-condition-icon
|
||||
slot="leading-icon"
|
||||
class="condition-icon"
|
||||
.path=${CONDITION_ICONS[this.condition.condition]}
|
||||
></ha-svg-icon>
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
@@ -395,9 +397,14 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
<ha-automation-condition-editor
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.description=${this.conditionDescriptions[
|
||||
this.condition.condition
|
||||
]}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
||||
.uiSupported=${this._uiSupported(
|
||||
this._getType(this.condition, this.conditionDescriptions)
|
||||
)}
|
||||
.narrow=${this.narrow}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-condition-editor>`
|
||||
@@ -476,7 +483,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.disabled=${this.disabled}
|
||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
||||
.uiSupported=${this._uiSupported(
|
||||
this._getType(this.condition, this.conditionDescriptions)
|
||||
)}
|
||||
indent
|
||||
.selected=${this._selected}
|
||||
.narrow=${this.narrow}
|
||||
@@ -786,7 +795,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
cut: this._cutCondition,
|
||||
test: this._testCondition,
|
||||
config: sidebarCondition,
|
||||
uiSupported: this._uiSupported(sidebarCondition.condition),
|
||||
uiSupported: this._uiSupported(
|
||||
this._getType(sidebarCondition, this.conditionDescriptions)
|
||||
),
|
||||
description: this.conditionDescriptions[sidebarCondition.condition],
|
||||
yamlMode: this._yamlMode,
|
||||
} satisfies ConditionSidebarConfig);
|
||||
this._selected = true;
|
||||
@@ -802,6 +814,16 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _getType = memoizeOne(
|
||||
(condition: Condition, conditionDescriptions: ConditionDescriptions) => {
|
||||
if (condition.condition in conditionDescriptions) {
|
||||
return "platform";
|
||||
}
|
||||
|
||||
return condition.condition;
|
||||
}
|
||||
);
|
||||
|
||||
private _uiSupported = memoizeOne(
|
||||
(type: string) =>
|
||||
customElements.get(`ha-automation-condition-${type}`) !== undefined
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
@@ -12,11 +13,18 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
import {
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type AutomationClipboard,
|
||||
type Condition,
|
||||
} from "../../../../data/automation";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
subscribeConditions,
|
||||
} from "../../../../data/condition";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
@@ -25,10 +33,9 @@ import {
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import "./ha-automation-condition-row";
|
||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
|
||||
@customElement("ha-automation-condition")
|
||||
export default class HaAutomationCondition extends LitElement {
|
||||
export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public conditions!: Condition[];
|
||||
@@ -46,6 +53,8 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
@state() private _rowSortSelected?: number;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
@@ -64,6 +73,26 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
private _conditionKeys = new WeakMap<Condition, string>();
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeConditions(this.hass, (conditions) =>
|
||||
this._addConditions(conditions)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _addConditions(conditions: ConditionDescriptions) {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("conditions")) {
|
||||
return;
|
||||
@@ -168,6 +197,7 @@ export default class HaAutomationCondition extends LitElement {
|
||||
.last=${idx === this.conditions.length - 1}
|
||||
.totalConditions=${this.conditions.length}
|
||||
.condition=${cond}
|
||||
.conditionDescriptions=${this._conditionDescriptions}
|
||||
.disabled=${this.disabled}
|
||||
.narrow=${this.narrow}
|
||||
@duplicate=${this._duplicateCondition}
|
||||
@@ -237,6 +267,10 @@ export default class HaAutomationCondition extends LitElement {
|
||||
conditions = this.conditions.concat(
|
||||
deepClone(this._clipboard!.condition)
|
||||
);
|
||||
} else if (isDynamic(value)) {
|
||||
conditions = this.conditions.concat({
|
||||
condition: getValueFromDynamic(value),
|
||||
});
|
||||
} else {
|
||||
const condition = value as Condition["condition"];
|
||||
const elClass = customElements.get(
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import type { PlatformCondition } from "../../../../../data/automation";
|
||||
import {
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
type ConditionDescription,
|
||||
} from "../../../../../data/condition";
|
||||
import type { IntegrationManifest } from "../../../../../data/integration";
|
||||
import { fetchIntegrationManifest } from "../../../../../data/integration";
|
||||
import type { TargetSelector } from "../../../../../data/selector";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||
|
||||
const showOptionalToggle = (field: ConditionDescription["fields"][string]) =>
|
||||
field.selector &&
|
||||
!field.required &&
|
||||
!("boolean" in field.selector && field.default);
|
||||
|
||||
@customElement("ha-automation-condition-platform")
|
||||
export class HaPlatformCondition extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public condition!: PlatformCondition;
|
||||
|
||||
@property({ attribute: false }) public description?: ConditionDescription;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _checkedKeys = new Set();
|
||||
|
||||
@state() private _manifest?: IntegrationManifest;
|
||||
|
||||
public static get defaultConfig(): PlatformCondition {
|
||||
return { condition: "" };
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
this.hass.loadBackendTranslation("selector");
|
||||
}
|
||||
if (!changedProperties.has("condition")) {
|
||||
return;
|
||||
}
|
||||
const oldValue = changedProperties.get("condition") as
|
||||
| undefined
|
||||
| this["condition"];
|
||||
|
||||
// Fetch the manifest if we have a condition selected and the condition domain changed.
|
||||
// If no condition is selected, clear the manifest.
|
||||
if (this.condition?.condition) {
|
||||
const domain = getConditionDomain(this.condition.condition);
|
||||
|
||||
const oldDomain = getConditionDomain(oldValue?.condition || "");
|
||||
|
||||
if (domain !== oldDomain) {
|
||||
this._fetchManifest(domain);
|
||||
}
|
||||
} else {
|
||||
this._manifest = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const domain = getConditionDomain(this.condition.condition);
|
||||
const conditionName = getConditionObjectId(this.condition.condition);
|
||||
|
||||
const description = this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.description`
|
||||
);
|
||||
|
||||
const conditionDesc = this.description;
|
||||
|
||||
const shouldRenderDataYaml = !conditionDesc?.fields;
|
||||
|
||||
const hasOptional = Boolean(
|
||||
conditionDesc?.fields &&
|
||||
Object.values(conditionDesc.fields).some((field) =>
|
||||
showOptionalToggle(field)
|
||||
)
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="description">
|
||||
${description ? html`<p>${description}</p>` : nothing}
|
||||
${this._manifest
|
||||
? html`<a
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._manifest.domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-icon-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
</div>
|
||||
${conditionDesc && "target" in conditionDesc
|
||||
? html`<ha-settings-row narrow>
|
||||
${hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: nothing}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_secondary"
|
||||
)}</span
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._targetSelector(conditionDesc.target)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this.condition?.target}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: nothing}
|
||||
${shouldRenderDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.service-control.action_data"
|
||||
)}
|
||||
.name=${"data"}
|
||||
.readOnly=${this.disabled}
|
||||
.defaultValue=${this.condition?.options}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: Object.entries(conditionDesc.fields).map(([fieldName, dataField]) =>
|
||||
this._renderField(
|
||||
fieldName,
|
||||
dataField,
|
||||
hasOptional,
|
||||
domain,
|
||||
conditionName
|
||||
)
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetSelector = memoizeOne(
|
||||
(targetSelector: TargetSelector["target"] | null | undefined) =>
|
||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
||||
);
|
||||
|
||||
private _renderField = (
|
||||
fieldName: string,
|
||||
dataField: ConditionDescription["fields"][string],
|
||||
hasOptional: boolean,
|
||||
domain: string | undefined,
|
||||
conditionName: string | undefined
|
||||
) => {
|
||||
const selector = dataField?.selector ?? { text: null };
|
||||
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
|
||||
return dataField.selector
|
||||
? html`<ha-settings-row narrow>
|
||||
${!showOptional
|
||||
? hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: nothing
|
||||
: html`<ha-checkbox
|
||||
.key=${fieldName}
|
||||
.checked=${this._checkedKeys.has(fieldName) ||
|
||||
(this.condition?.options &&
|
||||
this.condition.options[fieldName] !== undefined)}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.name`
|
||||
) || conditionName}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.description`
|
||||
)}</span
|
||||
>
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(fieldName) &&
|
||||
(!this.condition?.options ||
|
||||
this.condition.options[fieldName] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${selector}
|
||||
.context=${this._generateContext(dataField)}
|
||||
.key=${fieldName}
|
||||
@value-changed=${this._dataChanged}
|
||||
.value=${this.condition?.options
|
||||
? this.condition.options[fieldName]
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: nothing;
|
||||
};
|
||||
|
||||
private _generateContext(
|
||||
field: ConditionDescription["fields"][string]
|
||||
): Record<string, any> | undefined {
|
||||
if (!field.context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
for (const [context_key, data_key] of Object.entries(field.context)) {
|
||||
context[context_key] =
|
||||
data_key === "target"
|
||||
? this.condition.target
|
||||
: this.condition.options?.[data_key];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private _dataChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.isValid === false) {
|
||||
// Don't clear an object selector that returns invalid YAML
|
||||
return;
|
||||
}
|
||||
const key = (ev.currentTarget as any).key;
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
this.condition?.options?.[key] === value ||
|
||||
((!this.condition?.options || !(key in this.condition.options)) &&
|
||||
(value === "" || value === undefined))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = { ...this.condition?.options, [key]: value };
|
||||
|
||||
if (
|
||||
value === "" ||
|
||||
value === undefined ||
|
||||
(typeof value === "object" && !Object.keys(value).length)
|
||||
) {
|
||||
delete options[key];
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
options,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _targetChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
target: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _checkboxChanged(ev) {
|
||||
const checked = ev.currentTarget.checked;
|
||||
const key = ev.currentTarget.key;
|
||||
let options;
|
||||
|
||||
if (checked) {
|
||||
this._checkedKeys.add(key);
|
||||
const field =
|
||||
this.description &&
|
||||
Object.entries(this.description).find(([k, _value]) => k === key)?.[1];
|
||||
let defaultValue = field?.default;
|
||||
|
||||
if (
|
||||
defaultValue == null &&
|
||||
field?.selector &&
|
||||
"constant" in field.selector
|
||||
) {
|
||||
defaultValue = field.selector.constant?.value;
|
||||
}
|
||||
|
||||
if (
|
||||
defaultValue == null &&
|
||||
field?.selector &&
|
||||
"boolean" in field.selector
|
||||
) {
|
||||
defaultValue = false;
|
||||
}
|
||||
|
||||
if (defaultValue != null) {
|
||||
options = {
|
||||
...this.condition?.options,
|
||||
[key]: defaultValue,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
this._checkedKeys.delete(key);
|
||||
options = { ...this.condition?.options };
|
||||
delete options[key];
|
||||
}
|
||||
if (options) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
options,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.requestUpdate("_checkedKeys");
|
||||
}
|
||||
|
||||
private _localizeValueCallback = (key: string) => {
|
||||
if (!this.condition?.condition) {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${computeDomain(this.condition.condition)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
private async _fetchManifest(integration: string) {
|
||||
this._manifest = undefined;
|
||||
try {
|
||||
this._manifest = await fetchIntegrationManifest(this.hass, integration);
|
||||
} catch (_err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Unable to fetch integration manifest for ${integration}`);
|
||||
// Ignore if loading manifest fails. Probably bad JSON in manifest
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-settings-row {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
ha-settings-row[narrow] {
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-settings-row {
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
);
|
||||
}
|
||||
ha-service-picker,
|
||||
ha-entity-picker,
|
||||
ha-yaml-editor {
|
||||
display: block;
|
||||
margin: 0 var(--ha-space-4);
|
||||
}
|
||||
ha-yaml-editor {
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
p {
|
||||
margin: 0 var(--ha-space-4);
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
:host([hide-picker]) p {
|
||||
padding-top: 0;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
ha-checkbox {
|
||||
margin-left: calc(var(--ha-space-4) * -1);
|
||||
margin-inline-start: calc(var(--ha-space-4) * -1);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.help-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.description {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 2px;
|
||||
padding-inline-end: 2px;
|
||||
padding-inline-start: initial;
|
||||
}
|
||||
.description p {
|
||||
direction: ltr;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-condition-platform": HaPlatformCondition;
|
||||
}
|
||||
}
|
||||
@@ -188,6 +188,7 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
class="handle ${this._resizing ? "resizing" : ""}"
|
||||
@mousedown=${this._handleMouseDown}
|
||||
@touchstart=${this._handleMouseDown}
|
||||
@dblclick=${this._handleDoubleClick}
|
||||
@focus=${this._startKeyboardResizing}
|
||||
@blur=${this._stopKeyboardResizing}
|
||||
tabindex="0"
|
||||
@@ -258,6 +259,17 @@ 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) {
|
||||
// register event listeners for drag handling
|
||||
document.addEventListener("mousemove", this._handleMouseMove);
|
||||
@@ -422,5 +434,6 @@ declare global {
|
||||
deltaInPx: number;
|
||||
};
|
||||
"sidebar-resizing-stopped": undefined;
|
||||
"sidebar-reset-size": undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,6 +317,7 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
@sidebar-resized=${this._resizeSidebar}
|
||||
@sidebar-resizing-stopped=${this._stopResizeSidebar}
|
||||
@sidebar-reset-size=${this._resetSidebarWidth}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
@@ -700,6 +701,16 @@ export class HaManualAutomationEditor extends LitElement {
|
||||
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 {
|
||||
return [
|
||||
saveFabStyles,
|
||||
|
||||
@@ -16,11 +16,16 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import {
|
||||
testCondition,
|
||||
type ConditionSidebarConfig,
|
||||
import type {
|
||||
LegacyCondition,
|
||||
ConditionSidebarConfig,
|
||||
} from "../../../../data/automation";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import { testCondition } from "../../../../data/automation";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
} from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
@@ -84,14 +89,25 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
"ui.panel.config.automation.editor.conditions.condition"
|
||||
);
|
||||
|
||||
const domain =
|
||||
"condition" in this.config.config &&
|
||||
getConditionDomain(this.config.config.condition);
|
||||
const conditionName =
|
||||
"condition" in this.config.config &&
|
||||
getConditionObjectId(this.config.config.condition);
|
||||
|
||||
const title =
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${type}.label`
|
||||
) || type;
|
||||
`ui.panel.config.automation.editor.conditions.type.${type as LegacyCondition["condition"]}.label`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.name`
|
||||
) ||
|
||||
type;
|
||||
|
||||
const description = isBuildingBlock
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${type}.description.picker`
|
||||
`ui.panel.config.automation.editor.conditions.type.${type as LegacyCondition["condition"]}.description.picker`
|
||||
)
|
||||
: "";
|
||||
|
||||
@@ -282,6 +298,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.config.config}
|
||||
.description=${this.config.description}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from "../../../data/blueprint";
|
||||
import { showScriptEditor } from "../../../data/script";
|
||||
import { findRelated } from "../../../data/search";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
@@ -60,6 +61,7 @@ type BlueprintMetaDataPath = BlueprintMetaData & {
|
||||
error: boolean;
|
||||
type: "automation" | "script";
|
||||
fullpath: string;
|
||||
usageCount?: number;
|
||||
};
|
||||
|
||||
const createNewFunctions = {
|
||||
@@ -128,14 +130,20 @@ class HaBlueprintOverview extends LitElement {
|
||||
})
|
||||
private _filter = "";
|
||||
|
||||
@state() private _usageCounts: Record<string, number> = {};
|
||||
|
||||
private _usageCountRequest = 0;
|
||||
|
||||
private _processedBlueprints = memoizeOne(
|
||||
(
|
||||
blueprints: Record<string, Blueprints>,
|
||||
localize: LocalizeFunc
|
||||
localize: LocalizeFunc,
|
||||
usageCounts: Record<string, number>
|
||||
): BlueprintMetaDataPath[] => {
|
||||
const result: any[] = [];
|
||||
Object.entries(blueprints).forEach(([type, typeBlueprints]) =>
|
||||
Object.entries(typeBlueprints).forEach(([path, blueprint]) => {
|
||||
const fullpath = `${type}/${path}`;
|
||||
if ("error" in blueprint) {
|
||||
result.push({
|
||||
name: blueprint.error,
|
||||
@@ -145,7 +153,8 @@ class HaBlueprintOverview extends LitElement {
|
||||
),
|
||||
error: true,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
fullpath,
|
||||
usageCount: 0,
|
||||
});
|
||||
} else {
|
||||
result.push({
|
||||
@@ -156,7 +165,8 @@ class HaBlueprintOverview extends LitElement {
|
||||
),
|
||||
error: false,
|
||||
path,
|
||||
fullpath: `${type}/${path}`,
|
||||
fullpath,
|
||||
usageCount: usageCounts[fullpath] || 0,
|
||||
});
|
||||
}
|
||||
})
|
||||
@@ -189,6 +199,34 @@ class HaBlueprintOverview extends LitElement {
|
||||
filterable: true,
|
||||
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: {
|
||||
title: "fullpath",
|
||||
hidden: true,
|
||||
@@ -266,6 +304,7 @@ class HaBlueprintOverview extends LitElement {
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this._loadUsageCounts();
|
||||
if (this.route.path === "/import") {
|
||||
const url = extractSearchParam("blueprint_url");
|
||||
navigate("/config/blueprint/dashboard", { replace: true });
|
||||
@@ -275,6 +314,13 @@ class HaBlueprintOverview extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("blueprints")) {
|
||||
this._loadUsageCounts();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-tabs-subpage-data-table
|
||||
@@ -284,7 +330,11 @@ class HaBlueprintOverview extends LitElement {
|
||||
.route=${this.route}
|
||||
.tabs=${configSections.automations}
|
||||
.columns=${this._columns(this.hass.localize)}
|
||||
.data=${this._processedBlueprints(this.blueprints, this.hass.localize)}
|
||||
.data=${this._processedBlueprints(
|
||||
this.blueprints,
|
||||
this.hass.localize,
|
||||
this._usageCounts
|
||||
)}
|
||||
id="fullpath"
|
||||
.noDataText=${this.hass.localize(
|
||||
"ui.panel.config.blueprint.overview.no_blueprints"
|
||||
@@ -380,10 +430,51 @@ class HaBlueprintOverview extends LitElement {
|
||||
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>) {
|
||||
const blueprint = this._processedBlueprints(
|
||||
this.blueprints,
|
||||
this.hass.localize
|
||||
this.hass.localize,
|
||||
this._usageCounts
|
||||
).find((b) => b.fullpath === ev.detail.id)!;
|
||||
if (blueprint.error) {
|
||||
showAlertDialog(this, {
|
||||
@@ -397,6 +488,25 @@ class HaBlueprintOverview extends LitElement {
|
||||
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) => {
|
||||
navigate(
|
||||
`/config/${blueprint.domain}/dashboard?blueprint=${encodeURIComponent(
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } 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,
|
||||
@@ -17,6 +13,8 @@ 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 {
|
||||
@@ -34,10 +32,22 @@ class ConfigAnalytics extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
|
||||
"Home Assistant analytics"}
|
||||
>
|
||||
<div class="card-content">
|
||||
${error ? html`<div class="error">${error}</div>` : ""}
|
||||
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
|
||||
${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>
|
||||
<ha-analytics
|
||||
translation_key_panel="config"
|
||||
@analytics-preferences-changed=${this._preferencesChanged}
|
||||
@@ -45,26 +55,59 @@ class ConfigAnalytics extends LitElement {
|
||||
.analytics=${this._analyticsDetails}
|
||||
></ha-analytics>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._save}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="footer">
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.analytics.learn_more")}
|
||||
</ha-button>
|
||||
</div>
|
||||
${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>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.info"
|
||||
)}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/device-database/")}
|
||||
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>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -96,11 +139,25 @@ 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 {
|
||||
@@ -117,21 +174,10 @@ class ConfigAnalytics extends LitElement {
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
ha-card:not(:first-of-type) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.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"
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ import { mdiDotsVertical, mdiDownload } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import "../../../layouts/hass-subpage";
|
||||
@@ -14,6 +12,8 @@ import {
|
||||
downloadFileSupported,
|
||||
fileDownload,
|
||||
} from "../../../util/file_download";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("ha-config-section-analytics")
|
||||
class HaConfigSectionAnalytics extends LitElement {
|
||||
@@ -33,22 +33,19 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
>
|
||||
${downloadFileSupported(this.hass)
|
||||
? html`
|
||||
<ha-button-menu
|
||||
@action=${this._handleOverflowAction}
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleOverflowAction}
|
||||
slot="toolbar-icon"
|
||||
>
|
||||
<ha-icon-button slot="trigger" .path=${mdiDotsVertical}>
|
||||
</ha-icon-button>
|
||||
<ha-list-item graphic="icon">
|
||||
<ha-svg-icon
|
||||
slot="graphic"
|
||||
.path=${mdiDownload}
|
||||
></ha-svg-icon>
|
||||
<ha-dropdown-item .value=${"download_device_info"}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiDownload}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.analytics.download_device_info"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-button-menu>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
<div class="content">
|
||||
@@ -58,9 +55,16 @@ class HaConfigSectionAnalytics extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _handleOverflowAction(): Promise<void> {
|
||||
const signedPath = await getSignedPath(this.hass, "/api/analytics/devices");
|
||||
fileDownload(signedPath.path);
|
||||
private async _handleOverflowAction(
|
||||
ev: CustomEvent<{ item: { value: string } }>
|
||||
): Promise<void> {
|
||||
if (ev.detail.item.value === "download_device_info") {
|
||||
const signedPath = await getSignedPath(
|
||||
this.hass,
|
||||
"/api/analytics/devices"
|
||||
);
|
||||
fileDownload(signedPath.path);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
|
||||
import { mdiDotsVertical, mdiRefresh } from "@mdi/js";
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
@@ -6,13 +5,9 @@ import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-bar";
|
||||
import "../../../components/ha-button-menu";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-check-list-item";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-metric";
|
||||
import { extractApiErrorMessage } from "../../../data/hassio/common";
|
||||
import type {
|
||||
@@ -33,6 +28,9 @@ import "../../../layouts/hass-subpage";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import "../dashboard/ha-config-updates";
|
||||
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")
|
||||
class HaConfigSectionUpdates extends LitElement {
|
||||
@@ -73,24 +71,25 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._checkUpdates}
|
||||
></ha-icon-button>
|
||||
<ha-button-menu multi>
|
||||
<ha-dropdown @wa-select=${this._handleOverflowAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-check-list-item
|
||||
left
|
||||
@request-selected=${this._toggleSkipped}
|
||||
.selected=${this._showSkipped}
|
||||
|
||||
<ha-dropdown-item
|
||||
type="checkbox"
|
||||
value="show_skipped"
|
||||
.checked=${this._showSkipped}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.updates.show_skipped")}
|
||||
</ha-check-list-item>
|
||||
</ha-dropdown-item>
|
||||
${this._supervisorInfo
|
||||
? html`
|
||||
<li divider role="separator"></li>
|
||||
<ha-list-item
|
||||
@request-selected=${this._toggleBeta}
|
||||
<wa-divider></wa-divider>
|
||||
<ha-dropdown-item
|
||||
value="toggle_beta"
|
||||
.disabled=${this._supervisorInfo.channel === "dev"}
|
||||
>
|
||||
${this._supervisorInfo.channel === "stable"
|
||||
@@ -98,10 +97,10 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.updates.leave_beta"
|
||||
)}
|
||||
</ha-list-item>
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: ""}
|
||||
</ha-button-menu>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
<div class="content">
|
||||
<ha-card outlined>
|
||||
@@ -133,27 +132,19 @@ class HaConfigSectionUpdates extends LitElement {
|
||||
this._supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
|
||||
}
|
||||
|
||||
private _toggleSkipped(ev: CustomEvent<RequestSelectedDetail>): void {
|
||||
if (ev.detail.source !== "property") {
|
||||
return;
|
||||
}
|
||||
|
||||
this._showSkipped = !this._showSkipped;
|
||||
}
|
||||
|
||||
private async _toggleBeta(
|
||||
ev: CustomEvent<RequestSelectedDetail>
|
||||
private async _handleOverflowAction(
|
||||
ev: CustomEvent<{ item: { value: string } }>
|
||||
): Promise<void> {
|
||||
if (!shouldHandleRequestSelectedEvent(ev)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => this._setChannel("beta"),
|
||||
});
|
||||
} else {
|
||||
this._setChannel("stable");
|
||||
if (ev.detail.item.value === "toggle_beta") {
|
||||
if (this._supervisorInfo!.channel === "stable") {
|
||||
showJoinBetaDialog(this, {
|
||||
join: async () => this._setChannel("beta"),
|
||||
});
|
||||
} else {
|
||||
this._setChannel("stable");
|
||||
}
|
||||
} else if (ev.detail.item.value === "show_skipped") {
|
||||
this._showSkipped = !this._showSkipped;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
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";
|
||||
@@ -9,16 +11,61 @@ 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 LitElement {
|
||||
export class HaDeviceCard extends SubscribeMixin(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
|
||||
@@ -58,7 +105,7 @@ export class HaDeviceCard extends LitElement {
|
||||
<span class="hub"
|
||||
><a
|
||||
href="/config/devices/device/${this.device.via_device_id}"
|
||||
>${this._computeDeviceNameDislay(
|
||||
>${this._computeDeviceNameDisplay(
|
||||
this.device.via_device_id
|
||||
)}</a
|
||||
></span
|
||||
@@ -126,6 +173,34 @@ export class HaDeviceCard extends 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>
|
||||
@@ -139,7 +214,7 @@ export class HaDeviceCard extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _computeDeviceNameDislay(deviceId) {
|
||||
private _computeDeviceNameDisplay(deviceId: string) {
|
||||
const device = this.hass.devices[deviceId];
|
||||
return device
|
||||
? computeDeviceNameDisplay(device, this.hass)
|
||||
@@ -162,8 +237,26 @@ export class HaDeviceCard extends 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: 8px;
|
||||
margin-top: var(--ha-space-2);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.manuf,
|
||||
|
||||
@@ -12,7 +12,7 @@ import "../../../lovelace/editor/dashboard-strategy-editor/hui-dashboard-strateg
|
||||
import type { LovelaceDashboardConfigureStrategyDialogParams } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-configure-strategy")
|
||||
export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LovelaceDashboardConfigureStrategyDialogParams;
|
||||
@@ -97,6 +97,6 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-lovelace-dashboard-configure-strategy": DialogLovelaceDashboardDetail;
|
||||
"dialog-lovelace-dashboard-configure-strategy": DialogLovelaceDashboardConfigureStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,13 @@ import "../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||
import type {
|
||||
LovelaceDashboard,
|
||||
LovelaceDashboardCreateParams,
|
||||
LovelaceDashboardMutableParams,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
@@ -61,9 +58,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
if (!this._params || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
const defaultPanelUrlPath =
|
||||
this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
|
||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||
const isLovelaceDashboard = this._params.urlPath === "lovelace";
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -88,9 +85,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
||||
)
|
||||
: this._params.urlPath === "lovelace"
|
||||
: isLovelaceDashboard
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_default"
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_lovelace"
|
||||
)
|
||||
: html`
|
||||
<ha-form
|
||||
@@ -119,24 +116,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._toggleDefault}
|
||||
.disabled=${this._params.urlPath === "lovelace" &&
|
||||
defaultPanelUrlPath === "lovelace"}
|
||||
>
|
||||
${this._params.urlPath === defaultPanelUrlPath
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.remove_default"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default"
|
||||
)}
|
||||
</ha-button>
|
||||
: nothing}
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateDashboard}
|
||||
@@ -254,40 +236,6 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private async _toggleDefault() {
|
||||
const urlPath = this._params?.urlPath;
|
||||
if (!urlPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
// Add warning dialog to saying that this will change the default dashboard for all users
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_title"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_text"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
default_panel: urlPath === defaultPanel ? undefined : urlPath,
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
if (this._params?.urlPath && !this._params.dashboard?.id) {
|
||||
this.closeDialog();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiCheckCircleOutline,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiHomeCircleOutline,
|
||||
mdiHomeEdit,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
@@ -10,7 +11,6 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
@@ -29,6 +29,7 @@ import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||
import type { LovelacePanelConfig } from "../../../../data/lovelace";
|
||||
import type { LovelaceRawConfig } from "../../../../data/lovelace/config/types";
|
||||
import {
|
||||
@@ -45,7 +46,11 @@ import {
|
||||
fetchDashboards,
|
||||
updateDashboard,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import {
|
||||
DEFAULT_PANEL,
|
||||
getPanelIcon,
|
||||
getPanelTitle,
|
||||
} from "../../../../data/panel";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
@@ -56,12 +61,21 @@ import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
export const PANEL_DASHBOARDS = [
|
||||
"home",
|
||||
"light",
|
||||
"security",
|
||||
"climate",
|
||||
"energy",
|
||||
] as string[];
|
||||
|
||||
type DataTableItem = Pick<
|
||||
LovelaceDashboard,
|
||||
"icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path"
|
||||
> & {
|
||||
default: boolean;
|
||||
filename: string;
|
||||
localized_type: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
@@ -112,7 +126,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeGrouping?: string = "type";
|
||||
private _activeGrouping?: string = "localized_type";
|
||||
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-collapsed",
|
||||
@@ -167,7 +181,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
<ha-svg-icon
|
||||
.id="default-icon-${dashboard.title}"
|
||||
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
|
||||
.path=${mdiCheckCircleOutline}
|
||||
.path=${mdiHomeCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
.for="default-icon-${dashboard.title}"
|
||||
@@ -183,7 +197,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
},
|
||||
};
|
||||
|
||||
columns.type = {
|
||||
columns.localized_type = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.type"
|
||||
),
|
||||
@@ -253,7 +267,15 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
.items=${[
|
||||
...(this._canEdit(dashboard.url_path)
|
||||
{
|
||||
path: mdiHomeEdit,
|
||||
label: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.set_as_default"
|
||||
),
|
||||
action: () => this._handleSetAsDefault(dashboard),
|
||||
disabled: dashboard.default,
|
||||
},
|
||||
...(dashboard.type === "user_created"
|
||||
? [
|
||||
{
|
||||
path: mdiPencil,
|
||||
@@ -262,10 +284,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
),
|
||||
action: () => this._handleEdit(dashboard),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(this._canDelete(dashboard.url_path)
|
||||
? [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.delete"
|
||||
@@ -288,92 +306,43 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
private _getItems = memoize(
|
||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
|
||||
const defaultMode = (
|
||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||
).mode;
|
||||
const mode = (this.hass.panels?.lovelace?.config as LovelacePanelConfig)
|
||||
.mode;
|
||||
const isDefault = defaultUrlPath === "lovelace";
|
||||
const result: DataTableItem[] = [
|
||||
{
|
||||
icon: "mdi:view-dashboard",
|
||||
title: this.hass.localize("panel.states"),
|
||||
default: isDefault,
|
||||
show_in_sidebar: isDefault,
|
||||
show_in_sidebar: true,
|
||||
require_admin: false,
|
||||
url_path: "lovelace",
|
||||
mode: defaultMode,
|
||||
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||
type: this._localizeType("built_in"),
|
||||
mode: mode,
|
||||
filename: mode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||
type: "built_in",
|
||||
localized_type: this._localizeType("built_in"),
|
||||
},
|
||||
];
|
||||
if (isComponentLoaded(this.hass, "energy")) {
|
||||
result.push({
|
||||
icon: "mdi:lightning-bolt",
|
||||
title: this.hass.localize(`ui.panel.config.dashboard.energy.main`),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "energy",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.light) {
|
||||
result.push({
|
||||
icon: this.hass.panels.light.icon || "mdi:lamps",
|
||||
title: this.hass.localize("panel.light"),
|
||||
PANEL_DASHBOARDS.forEach((panel) => {
|
||||
const panelInfo = this.hass.panels[panel];
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const item: DataTableItem = {
|
||||
icon: getPanelIcon(panelInfo),
|
||||
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "light",
|
||||
url_path: panelInfo.url_path,
|
||||
filename: "",
|
||||
default: false,
|
||||
default: defaultUrlPath === panelInfo.url_path,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.security) {
|
||||
result.push({
|
||||
icon: this.hass.panels.security.icon || "mdi:security",
|
||||
title: this.hass.localize("panel.security"),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "security",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.climate) {
|
||||
result.push({
|
||||
icon: this.hass.panels.climate.icon || "mdi:home-thermometer",
|
||||
title: this.hass.localize("panel.climate"),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "climate",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.home) {
|
||||
result.push({
|
||||
icon: this.hass.panels.home.icon || "mdi:home",
|
||||
title: this.hass.localize("panel.home"),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "home",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
type: "built_in",
|
||||
localized_type: this._localizeType("built_in"),
|
||||
};
|
||||
result.push(item);
|
||||
});
|
||||
|
||||
result.push(
|
||||
...dashboards
|
||||
@@ -386,7 +355,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
filename: "",
|
||||
...dashboard,
|
||||
default: defaultUrlPath === dashboard.url_path,
|
||||
type: this._localizeType("user_created"),
|
||||
type: "user_created",
|
||||
localized_type: this._localizeType("user_created"),
|
||||
}) satisfies DataTableItem
|
||||
)
|
||||
);
|
||||
@@ -486,20 +456,32 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._openDetailDialog(dashboard, urlPath);
|
||||
}
|
||||
|
||||
private _canDelete(urlPath: string) {
|
||||
return ![
|
||||
"lovelace",
|
||||
"energy",
|
||||
"light",
|
||||
"security",
|
||||
"climate",
|
||||
"home",
|
||||
].includes(urlPath);
|
||||
}
|
||||
private _handleSetAsDefault = async (item: DataTableItem) => {
|
||||
if (item.default) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _canEdit(urlPath: string) {
|
||||
return !["light", "security", "climate", "home"].includes(urlPath);
|
||||
}
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
default_panel: item.url_path,
|
||||
});
|
||||
};
|
||||
|
||||
private _handleDelete = async (item: DataTableItem) => {
|
||||
const dashboard = this._dashboards.find(
|
||||
@@ -581,10 +563,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
private async _deleteDashboard(
|
||||
dashboard: LovelaceDashboard
|
||||
): Promise<boolean> {
|
||||
if (!this._canDelete(dashboard.url_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.confirm_delete_title",
|
||||
|
||||
@@ -270,6 +270,7 @@ export class HaManualScriptEditor extends LitElement {
|
||||
@value-changed=${this._sidebarConfigChanged}
|
||||
@sidebar-resized=${this._resizeSidebar}
|
||||
@sidebar-resizing-stopped=${this._stopResizeSidebar}
|
||||
@sidebar-reset-size=${this._resetSidebarWidth}
|
||||
></ha-automation-sidebar>
|
||||
</div>
|
||||
</div>
|
||||
@@ -618,6 +619,16 @@ export class HaManualScriptEditor extends LitElement {
|
||||
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 {
|
||||
return [
|
||||
saveFabStyles,
|
||||
|
||||
@@ -6,6 +6,7 @@ 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,
|
||||
@@ -20,6 +21,8 @@ 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 {
|
||||
@@ -37,8 +40,12 @@ 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}
|
||||
@@ -106,6 +113,7 @@ export class AssistPipelineDebug extends LitElement {
|
||||
? html`<assist-render-pipeline-events
|
||||
.hass=${this.hass}
|
||||
.events=${this._events}
|
||||
.chatLog=${this._chatLog}
|
||||
></assist-render-pipeline-events>`
|
||||
: ""}
|
||||
</div>
|
||||
@@ -120,6 +128,10 @@ 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;
|
||||
}
|
||||
@@ -135,6 +147,10 @@ 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() {
|
||||
@@ -185,8 +201,27 @@ 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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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";
|
||||
@@ -24,6 +25,8 @@ 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 {
|
||||
@@ -46,6 +49,13 @@ 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
|
||||
@@ -178,6 +188,7 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
<assist-render-pipeline-run
|
||||
.hass=${this.hass}
|
||||
.pipelineRun=${run}
|
||||
.chatLog=${this._chatLog}
|
||||
></assist-render-pipeline-run>
|
||||
`
|
||||
)}
|
||||
@@ -186,6 +197,14 @@ 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
|
||||
@@ -408,6 +427,32 @@ 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,
|
||||
|
||||
@@ -9,6 +9,7 @@ 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 {
|
||||
@@ -16,6 +17,8 @@ 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;
|
||||
@@ -56,6 +59,7 @@ export class AssistPipelineEvents extends LitElement {
|
||||
<assist-render-pipeline-run
|
||||
.hass=${this.hass}
|
||||
.pipelineRun=${run}
|
||||
.chatLog=${this.chatLog}
|
||||
></assist-render-pipeline-run>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-alert";
|
||||
@@ -12,6 +12,12 @@ 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"];
|
||||
@@ -119,7 +125,7 @@ const dataMinusKeysRender = (
|
||||
result[key] = data[key];
|
||||
}
|
||||
return render
|
||||
? html`<ha-expansion-panel>
|
||||
? html`<ha-expansion-panel class="yaml-expansion">
|
||||
<span slot="header"
|
||||
>${hass.localize("ui.panel.config.voice_assistants.debug.raw")}</span
|
||||
>
|
||||
@@ -134,6 +140,8 @@ export class AssistPipelineDebug extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public pipelineRun!: PipelineRun;
|
||||
|
||||
@property({ attribute: false }) public chatLog?: ChatLog;
|
||||
|
||||
private _audioElement?: HTMLAudioElement;
|
||||
|
||||
private get _isPlaying(): boolean {
|
||||
@@ -147,31 +155,47 @@ export class AssistPipelineDebug extends LitElement {
|
||||
) || "ready"
|
||||
: "ready";
|
||||
|
||||
const messages: { from: string; text: string }[] = [];
|
||||
let messages: ChatLogContent[];
|
||||
|
||||
const userMessage =
|
||||
(this.pipelineRun.init_options &&
|
||||
"text" in this.pipelineRun.init_options.input
|
||||
? this.pipelineRun.init_options.input.text
|
||||
: undefined) ||
|
||||
this.pipelineRun?.stt?.stt_output?.text ||
|
||||
this.pipelineRun?.intent?.intent_input;
|
||||
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 = [];
|
||||
|
||||
if (userMessage) {
|
||||
messages.push({
|
||||
from: "user",
|
||||
text: userMessage,
|
||||
});
|
||||
}
|
||||
// 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
|
||||
? this.pipelineRun.init_options.input.text
|
||||
: undefined) ||
|
||||
this.pipelineRun?.stt?.stt_output?.text ||
|
||||
this.pipelineRun?.intent?.intent_input;
|
||||
|
||||
if (
|
||||
this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
|
||||
) {
|
||||
messages.push({
|
||||
from: "hass",
|
||||
text: this.pipelineRun.intent.intent_output.response.speech.plain
|
||||
.speech,
|
||||
});
|
||||
if (userMessage) {
|
||||
messages.push({
|
||||
role: "user",
|
||||
content: userMessage,
|
||||
} as ChatLogUserContent);
|
||||
}
|
||||
|
||||
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`
|
||||
@@ -190,10 +214,58 @@ export class AssistPipelineDebug extends LitElement {
|
||||
${messages.length > 0
|
||||
? html`
|
||||
<div class="messages">
|
||||
${messages.map(
|
||||
({ from, text }) => html`
|
||||
<div class=${`message ${from}`}>${text}</div>
|
||||
`
|
||||
${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}
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
<div style="clear:both"></div>
|
||||
@@ -442,7 +514,7 @@ export class AssistPipelineDebug extends LitElement {
|
||||
: ""}
|
||||
${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
|
||||
<ha-card>
|
||||
<ha-expansion-panel>
|
||||
<ha-expansion-panel class="yaml-expansion">
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.debug.raw"
|
||||
@@ -519,12 +591,12 @@ export class AssistPipelineDebug extends LitElement {
|
||||
.row > div:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
.yaml-expansion {
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
.card-content ha-expansion-panel {
|
||||
.card-content .yaml-expansion {
|
||||
padding-left: 0px;
|
||||
padding-inline-start: 0px;
|
||||
padding-inline-end: initial;
|
||||
@@ -540,27 +612,59 @@ 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;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
.messages pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.user,
|
||||
.tool_result {
|
||||
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.hass {
|
||||
.message.user,
|
||||
.content-expansion div[slot="header"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.assistant {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
|
||||
import { navigate } from "../../common/navigate";
|
||||
import { computeTimelineColor } from "../../components/chart/timeline-color";
|
||||
import "../../components/entity/state-badge";
|
||||
@@ -463,15 +464,24 @@ class HaLogbookRenderer extends LitElement {
|
||||
entityName: string | undefined,
|
||||
noLink?: boolean
|
||||
) {
|
||||
const hasState = entityId && entityId in this.hass.states;
|
||||
const displayName =
|
||||
entityName ||
|
||||
(hasState
|
||||
? this.hass.states[entityId].attributes.friendly_name || entityId
|
||||
: entityId);
|
||||
if (!entityId) {
|
||||
return entityName || "";
|
||||
}
|
||||
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const hasState = Boolean(stateObj);
|
||||
|
||||
const displayName = hasState
|
||||
? this.hass.formatEntityName(stateObj, DEFAULT_ENTITY_NAME) ||
|
||||
entityName ||
|
||||
stateObj.attributes.friendly_name ||
|
||||
entityId
|
||||
: entityName || entityId;
|
||||
|
||||
if (!hasState) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
return noLink
|
||||
? displayName
|
||||
: html`<button
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getEnergyColor } from "./common/color";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import "./common/hui-energy-graph-chip";
|
||||
import type {
|
||||
EnergyData,
|
||||
EnergySumData,
|
||||
@@ -67,6 +68,8 @@ export class HuiEnergyUsageGraphCard
|
||||
|
||||
@state() private _compareEnd?: Date;
|
||||
|
||||
@state() private _total?: number;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
@@ -100,9 +103,19 @@ export class HuiEnergyUsageGraphCard
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_usage",
|
||||
{ num: formatNumber(this._total, this.hass.locale) }
|
||||
)}
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
@@ -338,6 +351,13 @@ export class HuiEnergyUsageGraphCard
|
||||
datasets.sort((a, b) => a.order - b.order);
|
||||
fillDataGapsAndRoundCaps(datasets);
|
||||
this._chartData = datasets;
|
||||
this._total = this._processTotal(consumption);
|
||||
}
|
||||
|
||||
private _processTotal(consumption: EnergyConsumptionData) {
|
||||
return consumption.total.used_total > 0
|
||||
? consumption.total.used_total
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _processDataSet(
|
||||
@@ -515,6 +535,9 @@ export class HuiEnergyUsageGraphCard
|
||||
height: 100%;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
|
||||
@@ -35,6 +35,7 @@ const COLORS: Record<HomeSummary, string> = {
|
||||
climate: "deep-orange",
|
||||
security: "blue-grey",
|
||||
media_players: "blue",
|
||||
unassigned_devices: "grey",
|
||||
};
|
||||
|
||||
@customElement("hui-home-summary-card")
|
||||
|
||||
@@ -50,6 +50,8 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
||||
"home-media-players": () =>
|
||||
import("./home/home-media-players-view-strategy"),
|
||||
"home-area": () => import("./home/home-area-view-strategy"),
|
||||
"home-unassigned-devices": () =>
|
||||
import("./home/home-unassigned-devices-view-strategy"),
|
||||
light: () => import("../../light/strategies/light-view-strategy"),
|
||||
security: () => import("../../security/strategies/security-view-strategy"),
|
||||
climate: () => import("../../climate/strategies/climate-view-strategy"),
|
||||
|
||||
@@ -9,6 +9,7 @@ export const HOME_SUMMARIES = [
|
||||
"climate",
|
||||
"security",
|
||||
"media_players",
|
||||
"unassigned_devices",
|
||||
] as const;
|
||||
|
||||
export type HomeSummary = (typeof HOME_SUMMARIES)[number];
|
||||
@@ -18,6 +19,7 @@ export const HOME_SUMMARIES_ICONS: Record<HomeSummary, string> = {
|
||||
climate: "mdi:home-thermometer",
|
||||
security: "mdi:security",
|
||||
media_players: "mdi:multimedia",
|
||||
unassigned_devices: "mdi:shape",
|
||||
};
|
||||
|
||||
export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
|
||||
@@ -25,6 +27,19 @@ export const HOME_SUMMARIES_FILTERS: Record<HomeSummary, EntityFilter[]> = {
|
||||
climate: climateEntityFilters,
|
||||
security: securityEntityFilters,
|
||||
media_players: [{ domain: "media_player", entity_category: "none" }],
|
||||
unassigned_devices: [
|
||||
{
|
||||
area: null,
|
||||
hidden_platform: [
|
||||
"automation",
|
||||
"script",
|
||||
"hassio",
|
||||
"backup",
|
||||
"zone",
|
||||
"person",
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const getSummaryLabel = (
|
||||
|
||||
@@ -71,6 +71,16 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
icon: HOME_SUMMARIES_ICONS.media_players,
|
||||
} satisfies LovelaceViewRawConfig;
|
||||
|
||||
const unassignedDevicesView = {
|
||||
title: getSummaryLabel(hass.localize, "unassigned_devices"),
|
||||
path: "unassigned-devices",
|
||||
subview: true,
|
||||
strategy: {
|
||||
type: "home-unassigned-devices",
|
||||
},
|
||||
icon: HOME_SUMMARIES_ICONS.unassigned_devices,
|
||||
} satisfies LovelaceViewRawConfig;
|
||||
|
||||
return {
|
||||
views: [
|
||||
{
|
||||
@@ -83,6 +93,7 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
},
|
||||
...areaViews,
|
||||
mediaPlayersView,
|
||||
unassignedDevicesView,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,6 +228,19 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
||||
columns: 4,
|
||||
},
|
||||
} satisfies HomeSummaryCard),
|
||||
{
|
||||
type: "home-summary",
|
||||
summary: "unassigned_devices",
|
||||
vertical: true,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "unassigned-devices",
|
||||
},
|
||||
grid_options: {
|
||||
rows: 2,
|
||||
columns: 4,
|
||||
},
|
||||
} satisfies HomeSummaryCard,
|
||||
].filter(Boolean) as LovelaceCardConfig[];
|
||||
|
||||
const summarySection: LovelaceSectionConfig = {
|
||||
@@ -297,6 +310,29 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
const noAreaFilter = generateEntityFilter(hass, {
|
||||
area: null,
|
||||
});
|
||||
|
||||
const otherEntities = allEntities.filter(noAreaFilter);
|
||||
|
||||
if (otherEntities.length > 0) {
|
||||
widgetSection.cards!.push({
|
||||
type: "tile",
|
||||
entity: otherEntities[0],
|
||||
icon: "mdi:shape",
|
||||
name: "Unassigned devices",
|
||||
hide_state: true,
|
||||
tap_action: {
|
||||
action: "navigate",
|
||||
navigation_path: "unassigned-devices",
|
||||
},
|
||||
icon_tap_action: {
|
||||
action: "none",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const sections = (
|
||||
[
|
||||
favoriteSection.cards && favoriteSection,
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
|
||||
import { getEntityContext } from "../../../../common/entity/context/get_entity_context";
|
||||
import {
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
} from "../../../../common/entity/entity_filter";
|
||||
import { clamp } from "../../../../common/number/clamp";
|
||||
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
|
||||
import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isHelperDomain } from "../../../config/helpers/const";
|
||||
import type { HeadingCardConfig } from "../../cards/types";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
|
||||
export interface HomeUnassignedDevicesViewStrategyConfig {
|
||||
type: "home-unassigned-devices";
|
||||
}
|
||||
|
||||
@customElement("home-unassigned-devices-view-strategy")
|
||||
export class HomeUnassignedDevicesViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
_config: HomeUnassignedDevicesViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const allEntities = Object.keys(hass.states);
|
||||
|
||||
const unassignedFilters = HOME_SUMMARIES_FILTERS.unassigned_devices.map(
|
||||
(filter) => generateEntityFilter(hass, filter)
|
||||
);
|
||||
|
||||
const unassignedEntities = findEntities(allEntities, unassignedFilters);
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
const entitiesByDevice: Record<string, string[]> = {};
|
||||
const entitiesWithoutDevices: string[] = [];
|
||||
for (const entityId of unassignedEntities) {
|
||||
const stateObj = hass.states[entityId];
|
||||
if (!stateObj) continue;
|
||||
const { device } = getEntityContext(
|
||||
stateObj,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors
|
||||
);
|
||||
if (!device) {
|
||||
entitiesWithoutDevices.push(entityId);
|
||||
continue;
|
||||
}
|
||||
if (!(device.id in entitiesByDevice)) {
|
||||
entitiesByDevice[device.id] = [];
|
||||
}
|
||||
entitiesByDevice[device.id].push(entityId);
|
||||
}
|
||||
|
||||
const devicesEntities = Object.entries(entitiesByDevice).map(
|
||||
([deviceId, entities]) => ({
|
||||
device_id: deviceId,
|
||||
entities: entities,
|
||||
})
|
||||
);
|
||||
|
||||
const helpersEntities = entitiesWithoutDevices.filter((entityId) => {
|
||||
const domain = entityId.split(".")[0];
|
||||
return isHelperDomain(domain);
|
||||
});
|
||||
|
||||
const otherEntities = entitiesWithoutDevices.filter((entityId) => {
|
||||
const domain = entityId.split(".")[0];
|
||||
return !isHelperDomain(domain);
|
||||
});
|
||||
|
||||
const batteryFilter = generateEntityFilter(hass, {
|
||||
domain: "sensor",
|
||||
device_class: "battery",
|
||||
});
|
||||
|
||||
const energyFilter = generateEntityFilter(hass, {
|
||||
domain: "sensor",
|
||||
device_class: ["energy", "power"],
|
||||
});
|
||||
|
||||
const primaryFilter = generateEntityFilter(hass, {
|
||||
entity_category: "none",
|
||||
});
|
||||
|
||||
for (const deviceEntities of devicesEntities) {
|
||||
if (deviceEntities.entities.length === 0) continue;
|
||||
|
||||
const batteryEntities = deviceEntities.entities.filter((e) =>
|
||||
batteryFilter(e)
|
||||
);
|
||||
const entities = deviceEntities.entities.filter(
|
||||
(e) => !batteryFilter(e) && !energyFilter(e) && primaryFilter(e)
|
||||
);
|
||||
|
||||
if (entities.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const deviceId = deviceEntities.device_id;
|
||||
const device = hass.devices[deviceId];
|
||||
let heading = "";
|
||||
if (device) {
|
||||
heading =
|
||||
computeDeviceName(device) ||
|
||||
hass.localize("ui.panel.lovelace.strategy.home.unamed_device");
|
||||
}
|
||||
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: heading,
|
||||
tap_action: device
|
||||
? {
|
||||
action: "navigate",
|
||||
navigation_path: `/config/devices/device/${device.id}`,
|
||||
}
|
||||
: undefined,
|
||||
badges: [
|
||||
...batteryEntities.slice(0, 1).map((e) => ({
|
||||
entity: e,
|
||||
type: "entity",
|
||||
tap_action: {
|
||||
action: "more-info",
|
||||
},
|
||||
})),
|
||||
],
|
||||
} satisfies HeadingCardConfig,
|
||||
...entities.map((e) => ({
|
||||
type: "tile",
|
||||
entity: e,
|
||||
name: {
|
||||
type: "entity",
|
||||
},
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (helpersEntities.length) {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize(
|
||||
"ui.panel.lovelace.strategy.unassigned_devices.unassigned_helpers"
|
||||
),
|
||||
} satisfies HeadingCardConfig,
|
||||
...helpersEntities.map((e) => ({
|
||||
type: "tile",
|
||||
entity: e,
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (otherEntities.length) {
|
||||
sections.push({
|
||||
type: "grid",
|
||||
cards: [
|
||||
{
|
||||
type: "heading",
|
||||
heading: hass.localize(
|
||||
"ui.panel.lovelace.strategy.unassigned_devices.unassigned_entities"
|
||||
),
|
||||
} satisfies HeadingCardConfig,
|
||||
...otherEntities.map((e) => ({
|
||||
type: "tile",
|
||||
entity: e,
|
||||
})),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
|
||||
const maxColumns = clamp(sections.length, 2, 3);
|
||||
|
||||
// Take the full width if there is only one section to avoid narrow header on desktop
|
||||
if (sections.length === 1) {
|
||||
sections[0].column_span = 2;
|
||||
}
|
||||
|
||||
return {
|
||||
type: "sections",
|
||||
header: {
|
||||
badges_position: "bottom",
|
||||
},
|
||||
max_columns: maxColumns,
|
||||
sections: sections,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"home-unassigned-devices-view-strategy": HomeUnassignedDevicesViewStrategy;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-divider";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-select";
|
||||
import "../../components/ha-settings-row";
|
||||
import { saveFrontendUserData } from "../../data/frontend";
|
||||
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { saveFrontendUserData } from "../../data/frontend";
|
||||
import { getPanelTitle } from "../../data/panel";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards";
|
||||
|
||||
const USE_SYSTEM_VALUE = "___use_system___";
|
||||
|
||||
@@ -47,12 +50,24 @@ class HaPickDashboardRow extends LitElement {
|
||||
<ha-list-item .value=${USE_SYSTEM_VALUE}>
|
||||
${this.hass.localize("ui.panel.profile.dashboard.system")}
|
||||
</ha-list-item>
|
||||
<ha-divider></ha-divider>
|
||||
<ha-list-item value="lovelace">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="home">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.home")}
|
||||
</ha-list-item>
|
||||
${PANEL_DASHBOARDS.map((panel) => {
|
||||
const panelInfo = this.hass.panels[panel] as
|
||||
| PanelInfo
|
||||
| undefined;
|
||||
if (!panelInfo) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-list-item value=${panelInfo.url_path}>
|
||||
${getPanelTitle(this.hass, panelInfo)}
|
||||
</ha-list-item>
|
||||
`;
|
||||
})}
|
||||
<ha-divider></ha-divider>
|
||||
${this._dashboards.map((dashboard) => {
|
||||
if (!this.hass.user!.is_admin && dashboard.require_admin) {
|
||||
return "";
|
||||
|
||||
@@ -2217,7 +2217,9 @@
|
||||
"sidebar_toggle": "Sidebar toggle",
|
||||
"edit_sidebar": "Edit sidebar",
|
||||
"edit_subtitle": "Synced on all devices",
|
||||
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device."
|
||||
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device.",
|
||||
"reset_to_defaults": "Reset to defaults",
|
||||
"reset_confirmation": "Are you sure you want to reset the sidebar to its default configuration? This will restore the original order and visibility of all panels."
|
||||
},
|
||||
"panel": {
|
||||
"home": {
|
||||
@@ -3508,6 +3510,7 @@
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"add_dashboard": "Add dashboard",
|
||||
"set_as_default": "Set as default",
|
||||
"type": {
|
||||
"user_created": "User created",
|
||||
"built_in": "Built-in"
|
||||
@@ -3516,7 +3519,7 @@
|
||||
"confirm_delete_title": "Delete {dashboard_title}?",
|
||||
"confirm_delete_text": "This dashboard will be permanently deleted.",
|
||||
"cant_edit_yaml": "Dashboards created in YAML cannot be edited from the UI. Change them in configuration.yaml.",
|
||||
"cant_edit_default": "The default dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.",
|
||||
"cant_edit_lovelace": "The Overview dashboard title and icon cannot be changed. You can create a new dashboard to get more customization options.",
|
||||
"detail": {
|
||||
"edit_dashboard": "Edit dashboard",
|
||||
"new_dashboard": "Add new dashboard",
|
||||
@@ -3533,9 +3536,7 @@
|
||||
"set_default": "Set as default",
|
||||
"remove_default": "Remove as default",
|
||||
"set_default_confirm_title": "Set as default dashboard?",
|
||||
"set_default_confirm_text": "This will replace the current default dashboard. Users can still override their default dashboard in their profile settings.",
|
||||
"remove_default_confirm_title": "Remove default dashboard?",
|
||||
"remove_default_confirm_text": "The default dashboard will be changed to Overview for every user. Users can still override their default dashboard in their profile settings."
|
||||
"set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant. Each user can change this in their profile."
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
@@ -4796,7 +4797,8 @@
|
||||
"headers": {
|
||||
"name": "Name",
|
||||
"type": "Type",
|
||||
"file_name": "File name"
|
||||
"file_name": "File name",
|
||||
"usage_count": "In use"
|
||||
},
|
||||
"types": {
|
||||
"automation": "Automation",
|
||||
@@ -6779,6 +6781,7 @@
|
||||
},
|
||||
"analytics": {
|
||||
"caption": "Analytics",
|
||||
"header": "Home Assistant analytics",
|
||||
"description": "Learn how to share data to improve Home Assistant",
|
||||
"preferences": {
|
||||
"base": {
|
||||
@@ -6796,10 +6799,21 @@
|
||||
"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 Foundation’s 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": "How we process your data",
|
||||
"learn_more": "Learn 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"
|
||||
},
|
||||
@@ -7049,7 +7063,8 @@
|
||||
},
|
||||
"home": {
|
||||
"summary_list": {
|
||||
"media_players": "Media players"
|
||||
"media_players": "Media players",
|
||||
"unassigned_devices": "Unassigned devices"
|
||||
},
|
||||
"welcome_user": "Welcome {user}",
|
||||
"summaries": "Summaries",
|
||||
@@ -7079,6 +7094,10 @@
|
||||
"home_media_players": {
|
||||
"media_players": "Media players",
|
||||
"other_media_players": "Other media players"
|
||||
},
|
||||
"unassigned_devices": {
|
||||
"unassigned_helpers": "Unassigned helpers",
|
||||
"unassigned_entities": "Unassigned entities"
|
||||
}
|
||||
},
|
||||
"cards": {
|
||||
@@ -7145,6 +7164,7 @@
|
||||
"energy_usage_graph": {
|
||||
"total_consumed": "Total consumed {num} kWh",
|
||||
"total_returned": "Total returned {num} kWh",
|
||||
"total_usage": "{num} kWh used",
|
||||
"combined_from_grid": "Combined from grid",
|
||||
"consumed_solar": "Consumed solar",
|
||||
"consumed_battery": "Consumed battery"
|
||||
|
||||
10
yarn.lock
10
yarn.lock
@@ -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.5.0"
|
||||
home-assistant-js-websocket: "npm:9.6.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.5.0":
|
||||
version: 9.5.0
|
||||
resolution: "home-assistant-js-websocket@npm:9.5.0"
|
||||
checksum: 10/42f991b3b85aa61be28984f099a001ac083fb3da54b2777283d0c97976c564a303d8d4ba467e1b8e29cbc33151cd6eef64c1a7d3392d62bbb9cbb27aa7ca9942
|
||||
"home-assistant-js-websocket@npm:9.6.0":
|
||||
version: 9.6.0
|
||||
resolution: "home-assistant-js-websocket@npm:9.6.0"
|
||||
checksum: 10/0eded7864632b5e19e92289ffac0e24308b1e8f425e292ae87ed21450852f7705db521e202614b1d5bbdb7948633143dce2524ed548db0c38486b40ed1ffa474
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user