mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-25 18:57:19 +00:00
Compare commits
18 Commits
helpers-en
...
sidebar_ho
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3781a7e8df | ||
|
|
4b595c684e | ||
|
|
1cf1d158d2 | ||
|
|
bedfddea19 | ||
|
|
f8b1dc2462 | ||
|
|
82a3db39fe | ||
|
|
254857b53f | ||
|
|
1648be6b83 | ||
|
|
c4c774c217 | ||
|
|
6a0aab2088 | ||
|
|
5a5593ec5b | ||
|
|
2d39afdeac | ||
|
|
974ac31277 | ||
|
|
47e98d532d | ||
|
|
2efc513221 | ||
|
|
5d820e3046 | ||
|
|
c3cff3bcd3 | ||
|
|
e65a8a6b66 |
@@ -112,7 +112,7 @@
|
|||||||
"google-timezones-json": "1.2.0",
|
"google-timezones-json": "1.2.0",
|
||||||
"gulp-zopfli-green": "6.0.2",
|
"gulp-zopfli-green": "6.0.2",
|
||||||
"hls.js": "1.6.14",
|
"hls.js": "1.6.14",
|
||||||
"home-assistant-js-websocket": "9.5.0",
|
"home-assistant-js-websocket": "9.6.0",
|
||||||
"idb-keyval": "6.2.2",
|
"idb-keyval": "6.2.2",
|
||||||
"intl-messageformat": "10.7.18",
|
"intl-messageformat": "10.7.18",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
|
|||||||
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);
|
font-size: var(--ha-font-size-xl);
|
||||||
line-height: var(--ha-line-height-condensed);
|
line-height: var(--ha-line-height-condensed);
|
||||||
font-weight: var(--ha-font-weight-medium);
|
font-weight: var(--ha-font-weight-medium);
|
||||||
|
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
|
||||||
}
|
}
|
||||||
.header-subtitle {
|
.header-subtitle {
|
||||||
font-size: var(--ha-font-size-m);
|
font-size: var(--ha-font-size-m);
|
||||||
line-height: var(--ha-line-height-normal);
|
line-height: var(--ha-line-height-normal);
|
||||||
color: var(--secondary-text-color);
|
color: var(
|
||||||
|
--ha-dialog-header-subtitle-color,
|
||||||
|
var(--secondary-text-color)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||||
.header-bar {
|
.header-bar {
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ export class HaExpansionPanel extends LitElement {
|
|||||||
::slotted([slot="header"]) {
|
::slotted([slot="header"]) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-wrap: anywhere;
|
overflow-wrap: anywhere;
|
||||||
|
color: var(--primary-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export class HaIconOverflowMenu extends LitElement {
|
|||||||
.path=${item.path}
|
.path=${item.path}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
${item.label}
|
${item.label}
|
||||||
</ha-md-menu-item> `
|
</ha-md-menu-item>`
|
||||||
)}
|
)}
|
||||||
</ha-md-button-menu>`
|
</ha-md-button-menu>`
|
||||||
: html`
|
: html`
|
||||||
@@ -103,6 +103,7 @@ export class HaIconOverflowMenu extends LitElement {
|
|||||||
:host {
|
:host {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
cursor: initial;
|
||||||
}
|
}
|
||||||
div[role="separator"] {
|
div[role="separator"] {
|
||||||
border-right: 1px solid var(--divider-color);
|
border-right: 1px solid var(--divider-color);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export interface DisplayItem {
|
|||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
disableSorting?: boolean;
|
disableSorting?: boolean;
|
||||||
|
disableHiding?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DisplayValue {
|
export interface DisplayValue {
|
||||||
@@ -101,6 +102,7 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
icon,
|
icon,
|
||||||
iconPath,
|
iconPath,
|
||||||
disableSorting,
|
disableSorting,
|
||||||
|
disableHiding,
|
||||||
} = item;
|
} = item;
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
@@ -155,18 +157,21 @@ export class HaItemDisplayEditor extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: nothing}
|
: nothing}
|
||||||
<ha-icon-button
|
${!isVisible || !disableHiding
|
||||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
? html`<ha-icon-button
|
||||||
slot="end"
|
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||||
.label=${this.hass.localize(
|
slot="end"
|
||||||
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
.label=${this.hass.localize(
|
||||||
{
|
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
||||||
label: label,
|
{
|
||||||
}
|
label: label,
|
||||||
)}
|
}
|
||||||
.value=${value}
|
)}
|
||||||
@click=${this._toggle}
|
.value=${value}
|
||||||
></ha-icon-button>
|
@click=${this._toggle}
|
||||||
|
.disabled=${disableHiding || false}
|
||||||
|
></ha-icon-button>`
|
||||||
|
: nothing}
|
||||||
${isVisible && !disableSorting
|
${isVisible && !disableSorting
|
||||||
? html`
|
? html`
|
||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ export class HaMdMenuItem extends MenuItemEl {
|
|||||||
::slotted([slot="headline"]) {
|
::slotted([slot="headline"]) {
|
||||||
text-wrap: nowrap;
|
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 { titleCase } from "../common/string/title-case";
|
||||||
import { fetchConfig } from "../data/lovelace/config/types";
|
import { fetchConfig } from "../data/lovelace/config/types";
|
||||||
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
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 type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||||
import "./ha-combo-box";
|
import "./ha-combo-box";
|
||||||
import type { HaComboBox } from "./ha-combo-box";
|
import type { HaComboBox } from "./ha-combo-box";
|
||||||
@@ -43,13 +43,8 @@ const createViewNavigationItem = (
|
|||||||
|
|
||||||
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||||
path: `/${panel.url_path}`,
|
path: `/${panel.url_path}`,
|
||||||
icon: panel.icon ?? "mdi:view-dashboard",
|
icon: getPanelIcon(panel) || "mdi:view-dashboard",
|
||||||
title:
|
title: getPanelTitle(hass, panel) || "",
|
||||||
panel.url_path === getDefaultPanelUrlPath(hass)
|
|
||||||
? hass.localize("panel.states")
|
|
||||||
: hass.localize(`panel.${panel.title}`) ||
|
|
||||||
panel.title ||
|
|
||||||
(panel.url_path ? titleCase(panel.url_path) : ""),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@customElement("ha-navigation-picker")
|
@customElement("ha-navigation-picker")
|
||||||
|
|||||||
@@ -1,22 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
mdiBell,
|
mdiBell,
|
||||||
mdiCalendar,
|
|
||||||
mdiCellphoneCog,
|
mdiCellphoneCog,
|
||||||
mdiChartBox,
|
|
||||||
mdiClipboardList,
|
|
||||||
mdiCog,
|
mdiCog,
|
||||||
mdiFormatListBulletedType,
|
|
||||||
mdiHammer,
|
|
||||||
mdiLightningBolt,
|
|
||||||
mdiMenu,
|
mdiMenu,
|
||||||
mdiMenuOpen,
|
mdiMenuOpen,
|
||||||
mdiPlayBoxMultiple,
|
|
||||||
mdiTooltipAccount,
|
|
||||||
mdiViewDashboard,
|
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||||
import { LitElement, css, html, nothing } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import {
|
import {
|
||||||
customElement,
|
customElement,
|
||||||
eventOptions,
|
eventOptions,
|
||||||
@@ -33,7 +24,14 @@ import { computeRTL } from "../common/util/compute_rtl";
|
|||||||
import { throttle } from "../common/util/throttle";
|
import { throttle } from "../common/util/throttle";
|
||||||
import { subscribeFrontendUserData } from "../data/frontend";
|
import { subscribeFrontendUserData } from "../data/frontend";
|
||||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
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 type { PersistentNotification } from "../data/persistent_notification";
|
||||||
import { subscribeNotifications } from "../data/persistent_notification";
|
import { subscribeNotifications } from "../data/persistent_notification";
|
||||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||||
@@ -54,8 +52,6 @@ import "./ha-spinner";
|
|||||||
import "./ha-svg-icon";
|
import "./ha-svg-icon";
|
||||||
import "./user/ha-user-badge";
|
import "./user/ha-user-badge";
|
||||||
|
|
||||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
|
||||||
|
|
||||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||||
|
|
||||||
const SORT_VALUE_URL_PATHS = {
|
const SORT_VALUE_URL_PATHS = {
|
||||||
@@ -67,18 +63,6 @@ const SORT_VALUE_URL_PATHS = {
|
|||||||
config: 11,
|
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 = (
|
const panelSorter = (
|
||||||
reverseSort: string[],
|
reverseSort: string[],
|
||||||
defaultPanel: string,
|
defaultPanel: string,
|
||||||
@@ -155,16 +139,23 @@ export const computePanels = memoizeOne(
|
|||||||
const beforeSpacer: PanelInfo[] = [];
|
const beforeSpacer: PanelInfo[] = [];
|
||||||
const afterSpacer: 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 (
|
if (
|
||||||
hiddenPanels.includes(panel.url_path) ||
|
!isDefaultPanel &&
|
||||||
(!panel.title && panel.url_path !== defaultPanel) ||
|
(!panel.title ||
|
||||||
(panel.default_visible === false &&
|
hiddenPanels.includes(panel.url_path) ||
|
||||||
!panelsOrder.includes(panel.url_path))
|
(panel.default_visible === false &&
|
||||||
|
!panelsOrder.includes(panel.url_path)))
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
(SHOW_AFTER_SPACER_PANELS.includes(panel.url_path)
|
||||||
? afterSpacer
|
? afterSpacer
|
||||||
: beforeSpacer
|
: beforeSpacer
|
||||||
).push(panel);
|
).push(panel);
|
||||||
@@ -251,10 +242,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the supervisor as being part of configuration
|
const selectedPanel = this.hass.panelUrl;
|
||||||
const selectedPanel = this.route.path?.startsWith("/hassio/")
|
|
||||||
? "config"
|
|
||||||
: this.hass.panelUrl;
|
|
||||||
|
|
||||||
// prettier-ignore
|
// prettier-ignore
|
||||||
return html`
|
return html`
|
||||||
@@ -397,9 +385,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
private _renderAllPanels(selectedPanel: string) {
|
private _renderAllPanels(selectedPanel: string) {
|
||||||
if (!this._panelOrder || !this._hiddenPanels) {
|
if (!this._panelOrder || !this._hiddenPanels) {
|
||||||
return html`
|
return html`
|
||||||
<ha-fade-in .delay=${500}
|
<ha-fade-in .delay=${500}>
|
||||||
><ha-spinner size="small"></ha-spinner
|
<ha-spinner size="small"></ha-spinner>
|
||||||
></ha-fade-in>
|
</ha-fade-in>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -413,7 +401,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
this.hass.locale
|
this.hass.locale
|
||||||
);
|
);
|
||||||
|
|
||||||
// prettier-ignore
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list
|
<ha-md-list
|
||||||
class="ha-scrollbar"
|
class="ha-scrollbar"
|
||||||
@@ -422,61 +409,42 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
@scroll=${this._listboxScroll}
|
@scroll=${this._listboxScroll}
|
||||||
@keydown=${this._listboxKeydown}
|
@keydown=${this._listboxKeydown}
|
||||||
>
|
>
|
||||||
${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)}
|
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||||
${this._renderSpacer()}
|
${this._renderSpacer()}
|
||||||
${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)}
|
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||||
${this._renderExternalConfiguration()}
|
${this.hass.user?.is_admin
|
||||||
|
? this._renderConfiguration(selectedPanel)
|
||||||
|
: this._renderExternalConfiguration()}
|
||||||
</ha-md-list>
|
</ha-md-list>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderPanels(
|
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||||
panels: PanelInfo[],
|
|
||||||
selectedPanel: string,
|
|
||||||
defaultPanel: string
|
|
||||||
) {
|
|
||||||
return panels.map((panel) =>
|
return panels.map((panel) =>
|
||||||
this._renderPanel(
|
this._renderPanel(panel, panel.url_path === selectedPanel)
|
||||||
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
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderPanel(
|
private _renderPanel(panel: PanelInfo, isSelected: boolean) {
|
||||||
urlPath: string,
|
const title = getPanelTitle(this.hass, panel);
|
||||||
title: string | null,
|
const urlPath = panel.url_path;
|
||||||
icon: string | null | undefined,
|
const icon = getPanelIcon(panel);
|
||||||
iconPath: string | null | undefined,
|
const iconPath = getPanelIconPath(panel);
|
||||||
selectedPanel: string
|
|
||||||
) {
|
return html`
|
||||||
return urlPath === "config"
|
<ha-md-list-item
|
||||||
? this._renderConfiguration(title, selectedPanel)
|
.href=${`/${urlPath}`}
|
||||||
: html`
|
type="link"
|
||||||
<ha-md-list-item
|
class=${classMap({ selected: isSelected })}
|
||||||
.href=${`/${urlPath}`}
|
@mouseenter=${this._itemMouseEnter}
|
||||||
type="link"
|
@mouseleave=${this._itemMouseLeave}
|
||||||
class=${classMap({
|
>
|
||||||
selected: selectedPanel === urlPath,
|
${iconPath
|
||||||
})}
|
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||||
@mouseenter=${this._itemMouseEnter}
|
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
||||||
@mouseleave=${this._itemMouseLeave}
|
<span class="item-text" slot="headline">${title}</span>
|
||||||
>
|
</ha-md-list-item>
|
||||||
${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() {
|
private _renderDivider() {
|
||||||
@@ -487,10 +455,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
return html`<div class="spacer" disabled></div>`;
|
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`
|
return html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
class="configuration${selectedPanel === "config" ? " selected" : ""}"
|
class="configuration ${classMap({ selected: isSelected })}"
|
||||||
type="button"
|
type="button"
|
||||||
href="/config"
|
href="/config"
|
||||||
@mouseenter=${this._itemMouseEnter}
|
@mouseenter=${this._itemMouseEnter}
|
||||||
@@ -504,15 +477,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
${this._updatesCount + this._issuesCount}
|
${this._updatesCount + this._issuesCount}
|
||||||
</span>
|
</span>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
<span class="item-text" slot="headline">${title}</span>
|
<span class="item-text" slot="headline"
|
||||||
|
>${this.hass.localize("panel.config")}</span
|
||||||
|
>
|
||||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||||
? html`
|
? html`
|
||||||
<span class="badge" slot="end"
|
<span class="badge" slot="end"
|
||||||
>${this._updatesCount + this._issuesCount}</span
|
>${this._updatesCount + this._issuesCount}</span
|
||||||
>
|
>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -535,19 +510,20 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
? html`
|
? html`
|
||||||
<span class="badge" slot="start"> ${notificationCount} </span>
|
<span class="badge" slot="start"> ${notificationCount} </span>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
<span class="item-text" slot="headline"
|
<span class="item-text" slot="headline"
|
||||||
>${this.hass.localize("ui.notification_drawer.title")}</span
|
>${this.hass.localize("ui.notification_drawer.title")}</span
|
||||||
>
|
>
|
||||||
${this.alwaysExpand && notificationCount > 0
|
${this.alwaysExpand && notificationCount > 0
|
||||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||||
: ""}
|
: nothing}
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderUserItem(selectedPanel: string) {
|
private _renderUserItem(selectedPanel: string) {
|
||||||
const isRTL = computeRTL(this.hass);
|
const isRTL = computeRTL(this.hass);
|
||||||
|
const isSelected = selectedPanel === "profile";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-md-list-item
|
<ha-md-list-item
|
||||||
@@ -555,7 +531,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
type="link"
|
type="link"
|
||||||
class=${classMap({
|
class=${classMap({
|
||||||
user: true,
|
user: true,
|
||||||
selected: selectedPanel === "profile",
|
selected: isSelected,
|
||||||
rtl: isRTL,
|
rtl: isRTL,
|
||||||
})}
|
})}
|
||||||
@mouseenter=${this._itemMouseEnter}
|
@mouseenter=${this._itemMouseEnter}
|
||||||
@@ -566,31 +542,30 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
|||||||
.user=${this.hass.user}
|
.user=${this.hass.user}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
></ha-user-badge>
|
></ha-user-badge>
|
||||||
|
<span class="item-text" slot="headline">
|
||||||
<span class="item-text" slot="headline"
|
${this.hass.user ? this.hass.user.name : ""}
|
||||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
</span>
|
||||||
>
|
|
||||||
</ha-md-list-item>
|
</ha-md-list-item>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _renderExternalConfiguration() {
|
private _renderExternalConfiguration() {
|
||||||
return html`${!this.hass.user?.is_admin &&
|
if (!this.hass.auth.external?.config.hasSettingsScreen) {
|
||||||
this.hass.auth.external?.config.hasSettingsScreen
|
return nothing;
|
||||||
? html`
|
}
|
||||||
<ha-md-list-item
|
return html`
|
||||||
@click=${this._handleExternalAppConfiguration}
|
<ha-md-list-item
|
||||||
type="button"
|
@click=${this._handleExternalAppConfiguration}
|
||||||
@mouseenter=${this._itemMouseEnter}
|
type="button"
|
||||||
@mouseleave=${this._itemMouseLeave}
|
@mouseenter=${this._itemMouseEnter}
|
||||||
>
|
@mouseleave=${this._itemMouseLeave}
|
||||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
>
|
||||||
<span class="item-text" slot="headline">
|
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
<span class="item-text" slot="headline">
|
||||||
</span>
|
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||||
</ha-md-list-item>
|
</span>
|
||||||
`
|
</ha-md-list-item>
|
||||||
: ""}`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleExternalAppConfiguration(ev: Event) {
|
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;
|
diagnostics?: boolean;
|
||||||
usage?: boolean;
|
usage?: boolean;
|
||||||
statistics?: boolean;
|
statistics?: boolean;
|
||||||
|
snapshots?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Analytics {
|
export interface Analytics {
|
||||||
|
|||||||
@@ -214,6 +214,8 @@ export interface PipelineRun {
|
|||||||
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
|
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
|
||||||
run: PipelineRunStartEvent["data"];
|
run: PipelineRunStartEvent["data"];
|
||||||
error?: PipelineErrorEvent["data"];
|
error?: PipelineErrorEvent["data"];
|
||||||
|
started: Date;
|
||||||
|
finished?: Date;
|
||||||
wake_word?: PipelineWakeWordStartEvent["data"] &
|
wake_word?: PipelineWakeWordStartEvent["data"] &
|
||||||
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
|
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
|
||||||
stt?: PipelineSTTStartEvent["data"] &
|
stt?: PipelineSTTStartEvent["data"] &
|
||||||
@@ -235,6 +237,7 @@ export const processEvent = (
|
|||||||
stage: "ready",
|
stage: "ready",
|
||||||
run: event.data,
|
run: event.data,
|
||||||
events: [event],
|
events: [event],
|
||||||
|
started: new Date(event.timestamp),
|
||||||
};
|
};
|
||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
@@ -290,9 +293,14 @@ export const processEvent = (
|
|||||||
tts: { ...run.tts!, ...event.data, done: true },
|
tts: { ...run.tts!, ...event.data, done: true },
|
||||||
};
|
};
|
||||||
} else if (event.type === "run-end") {
|
} else if (event.type === "run-end") {
|
||||||
run = { ...run, stage: "done" };
|
run = { ...run, finished: new Date(event.timestamp), stage: "done" };
|
||||||
} else if (event.type === "error") {
|
} 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 {
|
} else {
|
||||||
run = { ...run };
|
run = { ...run };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import type { LocalizeKeys } from "../common/translations/localize";
|
|||||||
import { createSearchParam } from "../common/url/search-params";
|
import { createSearchParam } from "../common/url/search-params";
|
||||||
import type { Context, HomeAssistant } from "../types";
|
import type { Context, HomeAssistant } from "../types";
|
||||||
import type { BlueprintInput } from "./blueprint";
|
import type { BlueprintInput } from "./blueprint";
|
||||||
|
import type { ConditionDescription } from "./condition";
|
||||||
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
||||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
import type { Action, Field, MODES } from "./script";
|
import type { Action, Field, MODES } from "./script";
|
||||||
@@ -236,6 +237,12 @@ interface BaseCondition {
|
|||||||
condition: string;
|
condition: string;
|
||||||
alias?: string;
|
alias?: string;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
|
options?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PlatformCondition extends BaseCondition {
|
||||||
|
condition: Exclude<string, LegacyCondition["condition"]>;
|
||||||
|
target?: HassServiceTarget;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LogicalCondition extends BaseCondition {
|
export interface LogicalCondition extends BaseCondition {
|
||||||
@@ -320,7 +327,7 @@ export type AutomationElementGroup = Record<
|
|||||||
{ icon?: string; members?: AutomationElementGroup }
|
{ icon?: string; members?: AutomationElementGroup }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export type Condition =
|
export type LegacyCondition =
|
||||||
| StateCondition
|
| StateCondition
|
||||||
| NumericStateCondition
|
| NumericStateCondition
|
||||||
| SunCondition
|
| SunCondition
|
||||||
@@ -331,6 +338,8 @@ export type Condition =
|
|||||||
| LogicalCondition
|
| LogicalCondition
|
||||||
| TriggerCondition;
|
| TriggerCondition;
|
||||||
|
|
||||||
|
export type Condition = LegacyCondition | PlatformCondition;
|
||||||
|
|
||||||
export type ConditionWithShorthand =
|
export type ConditionWithShorthand =
|
||||||
| Condition
|
| Condition
|
||||||
| ShorthandAndConditionList
|
| ShorthandAndConditionList
|
||||||
@@ -608,6 +617,7 @@ export interface ConditionSidebarConfig extends BaseSidebarConfig {
|
|||||||
insertAfter: (value: Condition | Condition[]) => boolean;
|
insertAfter: (value: Condition | Condition[]) => boolean;
|
||||||
toggleYamlMode: () => void;
|
toggleYamlMode: () => void;
|
||||||
config: Condition;
|
config: Condition;
|
||||||
|
description?: ConditionDescription;
|
||||||
yamlMode: boolean;
|
yamlMode: boolean;
|
||||||
uiSupported: boolean;
|
uiSupported: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,14 @@ import {
|
|||||||
} from "../common/string/format-list";
|
} from "../common/string/format-list";
|
||||||
import { hasTemplate } from "../common/string/has-template";
|
import { hasTemplate } from "../common/string/has-template";
|
||||||
import type { HomeAssistant } from "../types";
|
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 type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||||
import {
|
import {
|
||||||
localizeDeviceAutomationCondition,
|
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") {
|
if (condition.condition === "or") {
|
||||||
const conditions = ensureArray(condition.conditions);
|
const conditions = ensureArray(condition.conditions);
|
||||||
|
|
||||||
@@ -1287,12 +1327,5 @@ const tryDescribeCondition = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return undefined;
|
||||||
hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.conditions.type.${condition.condition}.label`
|
|
||||||
) ||
|
|
||||||
hass.localize(
|
|
||||||
`ui.panel.config.automation.editor.conditions.unknown_condition`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
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 {
|
import { mdiMapClock, mdiShape } from "@mdi/js";
|
||||||
mdiAmpersand,
|
import { computeDomain } from "../common/entity/compute_domain";
|
||||||
mdiClockOutline,
|
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||||
mdiCodeBraces,
|
import type { HomeAssistant } from "../types";
|
||||||
mdiDevices,
|
|
||||||
mdiGateOr,
|
|
||||||
mdiIdentifier,
|
|
||||||
mdiMapClock,
|
|
||||||
mdiMapMarkerRadius,
|
|
||||||
mdiNotEqualVariant,
|
|
||||||
mdiNumeric,
|
|
||||||
mdiShape,
|
|
||||||
mdiStateMachine,
|
|
||||||
mdiWeatherSunny,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import type { AutomationElementGroupCollection } from "./automation";
|
import type { AutomationElementGroupCollection } from "./automation";
|
||||||
|
import type { Selector, TargetSelector } from "./selector";
|
||||||
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,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||||
{
|
{
|
||||||
groups: {
|
groups: {
|
||||||
device: {},
|
device: {},
|
||||||
|
dynamicGroups: {},
|
||||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||||
time_location: {
|
time_location: {
|
||||||
icon: mdiMapClock,
|
icon: mdiMapClock,
|
||||||
@@ -62,3 +39,33 @@ export const COLLAPSIBLE_CONDITION_ELEMENTS = [
|
|||||||
"ha-automation-condition-not",
|
"ha-automation-condition-not",
|
||||||
"ha-automation-condition-or",
|
"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.locale,
|
||||||
hass.config
|
hass.config
|
||||||
);
|
);
|
||||||
|
collection.refresh();
|
||||||
scheduleUpdatePeriod();
|
scheduleUpdatePeriod();
|
||||||
},
|
},
|
||||||
addHours(
|
addHours(
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export interface CoreFrontendUserData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarFrontendUserData {
|
export interface SidebarFrontendUserData {
|
||||||
panelOrder: string[];
|
panelOrder?: string[];
|
||||||
hiddenPanels: string[];
|
hiddenPanels?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CoreFrontendSystemData {
|
export interface CoreFrontendSystemData {
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ import type {
|
|||||||
|
|
||||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||||
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
|
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
|
||||||
|
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||||
|
|
||||||
/** Icon to use when no icon specified for service. */
|
/** Icon to use when no icon specified for service. */
|
||||||
export const DEFAULT_SERVICE_ICON = mdiRoomService;
|
export const DEFAULT_SERVICE_ICON = mdiRoomService;
|
||||||
@@ -138,15 +139,25 @@ const resources: {
|
|||||||
all?: Promise<Record<string, TriggerIcons>>;
|
all?: Promise<Record<string, TriggerIcons>>;
|
||||||
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
|
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
|
||||||
};
|
};
|
||||||
|
conditions: {
|
||||||
|
all?: Promise<Record<string, ConditionIcons>>;
|
||||||
|
domains: Record<string, ConditionIcons | Promise<ConditionIcons>>;
|
||||||
|
};
|
||||||
} = {
|
} = {
|
||||||
entity: {},
|
entity: {},
|
||||||
entity_component: {},
|
entity_component: {},
|
||||||
services: { domains: {} },
|
services: { domains: {} },
|
||||||
triggers: { domains: {} },
|
triggers: { domains: {} },
|
||||||
|
conditions: { domains: {} },
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IconResources<
|
interface IconResources<
|
||||||
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
|
T extends
|
||||||
|
| ComponentIcons
|
||||||
|
| PlatformIcons
|
||||||
|
| ServiceIcons
|
||||||
|
| TriggerIcons
|
||||||
|
| ConditionIcons,
|
||||||
> {
|
> {
|
||||||
resources: Record<string, T>;
|
resources: Record<string, T>;
|
||||||
}
|
}
|
||||||
@@ -195,17 +206,24 @@ type TriggerIcons = Record<
|
|||||||
{ trigger: string; sections?: Record<string, string> }
|
{ trigger: string; sections?: Record<string, string> }
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
type ConditionIcons = Record<
|
||||||
|
string,
|
||||||
|
{ condition: string; sections?: Record<string, string> }
|
||||||
|
>;
|
||||||
|
|
||||||
export type IconCategory =
|
export type IconCategory =
|
||||||
| "entity"
|
| "entity"
|
||||||
| "entity_component"
|
| "entity_component"
|
||||||
| "services"
|
| "services"
|
||||||
| "triggers";
|
| "triggers"
|
||||||
|
| "conditions";
|
||||||
|
|
||||||
interface CategoryType {
|
interface CategoryType {
|
||||||
entity: PlatformIcons;
|
entity: PlatformIcons;
|
||||||
entity_component: ComponentIcons;
|
entity_component: ComponentIcons;
|
||||||
services: ServiceIcons;
|
services: ServiceIcons;
|
||||||
triggers: TriggerIcons;
|
triggers: TriggerIcons;
|
||||||
|
conditions: ConditionIcons;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getHassIcons = async <T extends IconCategory>(
|
export const getHassIcons = async <T extends IconCategory>(
|
||||||
@@ -327,6 +345,13 @@ export const getTriggerIcons = async (
|
|||||||
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> =>
|
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> =>
|
||||||
getCategoryIcons(hass, "triggers", domain, force);
|
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
|
// Cache for sorted range keys
|
||||||
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
|
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
|
||||||
|
|
||||||
@@ -526,6 +551,25 @@ export const triggerIcon = async (
|
|||||||
return icon;
|
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 (
|
export const serviceIcon = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
service: string
|
service: string
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { MediaSelectorValue } from "../../selector";
|
import type { MediaSelectorValue } from "../../selector";
|
||||||
import type { LovelaceBadgeConfig } from "./badge";
|
import type { LovelaceBadgeConfig } from "./badge";
|
||||||
import type { LovelaceCardConfig } from "./card";
|
import type { LovelaceCardConfig } from "./card";
|
||||||
import type { LovelaceSectionRawConfig } from "./section";
|
import type {
|
||||||
|
LovelaceSectionConfig,
|
||||||
|
LovelaceSectionRawConfig,
|
||||||
|
} from "./section";
|
||||||
import type { LovelaceStrategyConfig } from "./strategy";
|
import type { LovelaceStrategyConfig } from "./strategy";
|
||||||
|
|
||||||
export interface ShowViewConfig {
|
export interface ShowViewConfig {
|
||||||
@@ -33,6 +36,12 @@ export interface LovelaceViewHeaderConfig {
|
|||||||
badges_wrap?: "wrap" | "scroll";
|
badges_wrap?: "wrap" | "scroll";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LovelaceViewSidebarConfig {
|
||||||
|
sections?: LovelaceSectionConfig[];
|
||||||
|
content_label?: string;
|
||||||
|
sidebar_label?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface LovelaceBaseViewConfig {
|
export interface LovelaceBaseViewConfig {
|
||||||
index?: number;
|
index?: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -56,6 +65,8 @@ export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
|
|||||||
cards?: LovelaceCardConfig[];
|
cards?: LovelaceCardConfig[];
|
||||||
sections?: LovelaceSectionRawConfig[];
|
sections?: LovelaceSectionRawConfig[];
|
||||||
header?: LovelaceViewHeaderConfig;
|
header?: LovelaceViewHeaderConfig;
|
||||||
|
// Only used for section view, it should move to a section view config type when the views will have dedicated editor.
|
||||||
|
sidebar?: LovelaceViewSidebarConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {
|
export interface LovelaceStrategyViewConfig extends LovelaceBaseViewConfig {
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
import {
|
||||||
|
mdiAccount,
|
||||||
|
mdiCalendar,
|
||||||
|
mdiChartBox,
|
||||||
|
mdiClipboardList,
|
||||||
|
mdiFormatListBulletedType,
|
||||||
|
mdiHammer,
|
||||||
|
mdiLightningBolt,
|
||||||
|
mdiPlayBoxMultiple,
|
||||||
|
mdiTooltipAccount,
|
||||||
|
mdiViewDashboard,
|
||||||
|
} from "@mdi/js";
|
||||||
import type { HomeAssistant, PanelInfo } from "../types";
|
import type { HomeAssistant, PanelInfo } from "../types";
|
||||||
|
|
||||||
/** Panel to show when no panel is picked. */
|
/** Panel to show when no panel is picked. */
|
||||||
@@ -60,7 +72,7 @@ export const getPanelTitleFromUrlPath = (
|
|||||||
return getPanelTitle(hass, panel);
|
return getPanelTitle(hass, panel);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPanelIcon = (panel: PanelInfo): string | null => {
|
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
|
||||||
if (!panel.icon) {
|
if (!panel.icon) {
|
||||||
switch (panel.component_name) {
|
switch (panel.component_name) {
|
||||||
case "profile":
|
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"
|
| "preview_features"
|
||||||
| "selector"
|
| "selector"
|
||||||
| "services"
|
| "services"
|
||||||
| "triggers";
|
| "triggers"
|
||||||
|
| "conditions";
|
||||||
|
|
||||||
export const subscribeTranslationPreferences = (
|
export const subscribeTranslationPreferences = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import { LitElement, css, html, nothing } from "lit";
|
|||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import "../../components/ha-alert";
|
import "../../components/ha-alert";
|
||||||
import "../../components/ha-icon";
|
import "../../components/ha-icon";
|
||||||
import "../../components/ha-list-item";
|
import "../../components/ha-md-list-item";
|
||||||
import "../../components/ha-spinner";
|
import "../../components/ha-spinner";
|
||||||
import type {
|
import type {
|
||||||
ExternalEntityAddToActions,
|
|
||||||
ExternalEntityAddToAction,
|
ExternalEntityAddToAction,
|
||||||
|
ExternalEntityAddToActions,
|
||||||
} from "../../external_app/external_messaging";
|
} from "../../external_app/external_messaging";
|
||||||
import { showToast } from "../../util/toast";
|
import { showToast } from "../../util/toast";
|
||||||
|
|
||||||
import type { HomeAssistant } from "../../types";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
|
import type { HomeAssistant } from "../../types";
|
||||||
|
|
||||||
@customElement("ha-more-info-add-to")
|
@customElement("ha-more-info-add-to")
|
||||||
export class HaMoreInfoAddTo extends LitElement {
|
export class HaMoreInfoAddTo extends LitElement {
|
||||||
@@ -93,19 +93,18 @@ export class HaMoreInfoAddTo extends LitElement {
|
|||||||
<div class="actions-list">
|
<div class="actions-list">
|
||||||
${this._externalActions.actions.map(
|
${this._externalActions.actions.map(
|
||||||
(action) => html`
|
(action) => html`
|
||||||
<ha-list-item
|
<ha-md-list-item
|
||||||
graphic="icon"
|
type="button"
|
||||||
.disabled=${!action.enabled}
|
.disabled=${!action.enabled}
|
||||||
.action=${action}
|
.action=${action}
|
||||||
.twoline=${!!action.details}
|
|
||||||
@click=${this._actionSelected}
|
@click=${this._actionSelected}
|
||||||
>
|
>
|
||||||
|
<ha-icon slot="start" .icon=${action.mdi_icon}></ha-icon>
|
||||||
<span>${action.name}</span>
|
<span>${action.name}</span>
|
||||||
${action.details
|
${action.details
|
||||||
? html`<span slot="secondary">${action.details}</span>`
|
? html`<span slot="supporting-text">${action.details}</span>`
|
||||||
: nothing}
|
: nothing}
|
||||||
<ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
|
</ha-md-list-item>
|
||||||
</ha-list-item>
|
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -131,15 +130,6 @@ export class HaMoreInfoAddTo extends LitElement {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
ha-list-item {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-list-item[disabled] {
|
|
||||||
cursor: not-allowed;
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-icon {
|
ha-icon {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
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 { css, html, LitElement, nothing, type TemplateResult } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@@ -9,18 +9,30 @@ import "../../components/ha-dialog-header";
|
|||||||
import "../../components/ha-fade-in";
|
import "../../components/ha-fade-in";
|
||||||
import "../../components/ha-icon-button";
|
import "../../components/ha-icon-button";
|
||||||
import "../../components/ha-items-display-editor";
|
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 "../../components/ha-md-dialog";
|
||||||
import type { HaMdDialog } from "../../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-spinner";
|
||||||
|
import "../../components/ha-svg-icon";
|
||||||
import {
|
import {
|
||||||
fetchFrontendUserData,
|
fetchFrontendUserData,
|
||||||
saveFrontendUserData,
|
saveFrontendUserData,
|
||||||
} from "../../data/frontend";
|
} from "../../data/frontend";
|
||||||
|
import {
|
||||||
|
getDefaultPanelUrlPath,
|
||||||
|
getPanelIcon,
|
||||||
|
getPanelIconPath,
|
||||||
|
getPanelTitle,
|
||||||
|
SHOW_AFTER_SPACER_PANELS,
|
||||||
|
} from "../../data/panel";
|
||||||
import type { HomeAssistant } from "../../types";
|
import type { HomeAssistant } from "../../types";
|
||||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||||
import { getDefaultPanelUrlPath } from "../../data/panel";
|
|
||||||
|
|
||||||
@customElement("dialog-edit-sidebar")
|
@customElement("dialog-edit-sidebar")
|
||||||
class DialogEditSidebar extends LitElement {
|
class DialogEditSidebar extends LitElement {
|
||||||
@@ -105,48 +117,53 @@ class DialogEditSidebar extends LitElement {
|
|||||||
this.hass.locale
|
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) {
|
for (const panel of panels) {
|
||||||
if (
|
if (
|
||||||
panel.default_visible === false &&
|
panel.default_visible === false &&
|
||||||
!this._order.includes(panel.url_path) &&
|
!orderSet.has(panel.url_path) &&
|
||||||
!this._hidden.includes(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 = [
|
const items = [
|
||||||
...beforeSpacer,
|
...beforeSpacer,
|
||||||
...panels.filter((panel) => this._hidden!.includes(panel.url_path)),
|
...panels.filter((panel) => hiddenPanels.includes(panel.url_path)),
|
||||||
...afterSpacer.filter((panel) => panel.url_path !== "config"),
|
...afterSpacer,
|
||||||
].map((panel) => ({
|
].map<DisplayItem>((panel) => ({
|
||||||
value: panel.url_path,
|
value: panel.url_path,
|
||||||
label:
|
label:
|
||||||
panel.url_path === defaultPanel
|
(getPanelTitle(this.hass, panel) || panel.url_path) +
|
||||||
? panel.title || this.hass.localize("panel.states")
|
`${defaultPanel === panel.url_path ? " (default)" : ""}`,
|
||||||
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
|
icon: getPanelIcon(panel),
|
||||||
icon: panel.icon || undefined,
|
iconPath: getPanelIconPath(panel),
|
||||||
iconPath:
|
disableSorting: SHOW_AFTER_SPACER_PANELS.includes(panel.url_path),
|
||||||
panel.url_path === defaultPanel && !panel.icon
|
disableHiding: panel.url_path === defaultPanel,
|
||||||
? PANEL_ICONS.lovelace
|
|
||||||
: panel.url_path in PANEL_ICONS
|
|
||||||
? PANEL_ICONS[panel.url_path]
|
|
||||||
: undefined,
|
|
||||||
disableSorting: panel.url_path === "developer-tools",
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return html`<ha-items-display-editor
|
return html`
|
||||||
.hass=${this.hass}
|
<ha-items-display-editor
|
||||||
.value=${{
|
.hass=${this.hass}
|
||||||
order: this._order,
|
.value=${{
|
||||||
hidden: this._hidden,
|
order: this._order,
|
||||||
}}
|
hidden: hiddenPanels,
|
||||||
.items=${items}
|
}}
|
||||||
@value-changed=${this._changed}
|
.items=${items}
|
||||||
dont-sort-visible
|
@value-changed=${this._changed}
|
||||||
>
|
dont-sort-visible
|
||||||
</ha-items-display-editor>`;
|
>
|
||||||
|
</ha-items-display-editor>
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
@@ -171,6 +188,22 @@ class DialogEditSidebar extends LitElement {
|
|||||||
>${this.hass.localize("ui.sidebar.edit_subtitle")}</span
|
>${this.hass.localize("ui.sidebar.edit_subtitle")}</span
|
||||||
>`
|
>`
|
||||||
: nothing}
|
: 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>
|
</ha-dialog-header>
|
||||||
<div slot="content" class="content">${this._renderContent()}</div>
|
<div slot="content" class="content">${this._renderContent()}</div>
|
||||||
<div slot="actions">
|
<div slot="actions">
|
||||||
@@ -194,6 +227,26 @@ class DialogEditSidebar extends LitElement {
|
|||||||
this._hidden = [...hidden];
|
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() {
|
private async _save() {
|
||||||
if (this._migrateToUserData) {
|
if (this._migrateToUserData) {
|
||||||
const confirmation = await showConfirmationDialog(this, {
|
const confirmation = await showConfirmationDialog(this, {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { listenMediaQuery } from "../common/dom/media_query";
|
|||||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
||||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||||
import "../components/ha-drawer";
|
import "../components/ha-drawer";
|
||||||
|
import "../components/ha-snowflakes";
|
||||||
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
|
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
|
||||||
import type { HomeAssistant, Route } from "../types";
|
import type { HomeAssistant, Route } from "../types";
|
||||||
import "./partial-panel-resolver";
|
import "./partial-panel-resolver";
|
||||||
@@ -50,6 +51,7 @@ export class HomeAssistantMain extends LitElement {
|
|||||||
this.hass.panels && this.hass.userData && this.hass.systemData;
|
this.hass.panels && this.hass.userData && this.hass.systemData;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
|
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
|
||||||
<ha-drawer
|
<ha-drawer
|
||||||
.type=${sidebarNarrow ? "modal" : ""}
|
.type=${sidebarNarrow ? "modal" : ""}
|
||||||
.open=${sidebarNarrow ? this._drawerOpen : false}
|
.open=${sidebarNarrow ? this._drawerOpen : false}
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import { css, html, LitElement, nothing } from "lit";
|
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 memoizeOne from "memoize-one";
|
||||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||||
import { stringCompare } from "../../../../../common/string/compare";
|
|
||||||
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
|
||||||
|
import { stringCompare } from "../../../../../common/string/compare";
|
||||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||||
|
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
|
||||||
import "../../../../../components/ha-list-item";
|
import "../../../../../components/ha-list-item";
|
||||||
import "../../../../../components/ha-select";
|
import "../../../../../components/ha-select";
|
||||||
import type { HaSelect } from "../../../../../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 {
|
import {
|
||||||
CONDITION_BUILDING_BLOCKS,
|
CONDITION_BUILDING_BLOCKS,
|
||||||
CONDITION_ICONS,
|
getConditionDomain,
|
||||||
|
getConditionObjectId,
|
||||||
|
subscribeConditions,
|
||||||
} from "../../../../../data/condition";
|
} 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 "../../condition/ha-automation-condition-editor";
|
||||||
import type HaAutomationConditionEditor from "../../condition/ha-automation-condition-editor";
|
import type HaAutomationConditionEditor from "../../condition/ha-automation-condition-editor";
|
||||||
import "../../condition/types/ha-automation-condition-and";
|
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";
|
import type { ActionElement } from "../ha-automation-action-row";
|
||||||
|
|
||||||
@customElement("ha-automation-action-condition")
|
@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({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ type: Boolean }) public disabled = false;
|
@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;
|
@property({ type: Boolean, attribute: "indent" }) public indent = false;
|
||||||
|
|
||||||
|
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||||
|
|
||||||
@query("ha-automation-condition-editor")
|
@query("ha-automation-condition-editor")
|
||||||
private _conditionEditor?: HaAutomationConditionEditor;
|
private _conditionEditor?: HaAutomationConditionEditor;
|
||||||
|
|
||||||
@@ -50,6 +65,21 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
|||||||
return { condition: "state" };
|
return { condition: "state" };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected hassSubscribe() {
|
||||||
|
return [
|
||||||
|
subscribeConditions(this.hass, (conditions) =>
|
||||||
|
this._addConditions(conditions)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addConditions(conditions: ConditionDescriptions) {
|
||||||
|
this._conditionDescriptions = {
|
||||||
|
...this._conditionDescriptions,
|
||||||
|
...conditions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
|
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
|
||||||
this.action.condition
|
this.action.condition
|
||||||
@@ -64,19 +94,25 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
|||||||
"ui.panel.config.automation.editor.conditions.type_select"
|
"ui.panel.config.automation.editor.conditions.type_select"
|
||||||
)}
|
)}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.value=${this.action.condition}
|
.value=${this.action.condition in this._conditionDescriptions
|
||||||
|
? `${DYNAMIC_PREFIX}${this.action.condition}`
|
||||||
|
: this.action.condition}
|
||||||
naturalMenuWidth
|
naturalMenuWidth
|
||||||
@selected=${this._typeChanged}
|
@selected=${this._typeChanged}
|
||||||
@closed=${stopPropagation}
|
@closed=${stopPropagation}
|
||||||
>
|
>
|
||||||
${this._processedTypes(this.hass.localize).map(
|
${this._processedTypes(
|
||||||
([opt, label, icon]) => html`
|
this._conditionDescriptions,
|
||||||
|
this.hass.localize
|
||||||
|
).map(
|
||||||
|
([opt, label, condition]) => html`
|
||||||
<ha-list-item .value=${opt} graphic="icon">
|
<ha-list-item .value=${opt} graphic="icon">
|
||||||
${label}<ha-svg-icon
|
${label}
|
||||||
|
<ha-condition-icon
|
||||||
slot="graphic"
|
slot="graphic"
|
||||||
.path=${icon}
|
.condition=${condition}
|
||||||
></ha-svg-icon
|
></ha-condition-icon>
|
||||||
></ha-list-item>
|
</ha-list-item>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
</ha-select>
|
</ha-select>
|
||||||
@@ -88,11 +124,14 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
|||||||
? html`
|
? html`
|
||||||
<ha-automation-condition-editor
|
<ha-automation-condition-editor
|
||||||
.condition=${this.action}
|
.condition=${this.action}
|
||||||
|
.description=${this._conditionDescriptions[this.action.condition]}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@value-changed=${this._conditionChanged}
|
@value-changed=${this._conditionChanged}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
.uiSupported=${this._uiSupported(this.action.condition)}
|
.uiSupported=${this._uiSupported(
|
||||||
|
this._getType(this.action, this._conditionDescriptions)
|
||||||
|
)}
|
||||||
.indent=${this.indent}
|
.indent=${this.indent}
|
||||||
action
|
action
|
||||||
></ha-automation-condition-editor>
|
></ha-automation-condition-editor>
|
||||||
@@ -102,19 +141,46 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _processedTypes = memoizeOne(
|
private _processedTypes = memoizeOne(
|
||||||
(localize: LocalizeFunc): [string, string, string][] =>
|
(
|
||||||
(Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
|
conditionDescriptions: ConditionDescriptions,
|
||||||
.map(
|
localize: LocalizeFunc
|
||||||
([condition, icon]) =>
|
): [string, string, string][] => {
|
||||||
[
|
const legacy = (
|
||||||
condition,
|
Object.keys(CONDITION_ICONS) as (keyof typeof CONDITION_ICONS)[]
|
||||||
localize(
|
).map(
|
||||||
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
|
(condition) =>
|
||||||
),
|
[
|
||||||
icon,
|
condition,
|
||||||
] as [string, string, string]
|
localize(
|
||||||
)
|
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
|
||||||
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
|
),
|
||||||
|
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) {
|
private _conditionChanged(ev: CustomEvent) {
|
||||||
@@ -132,6 +198,18 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
|||||||
return;
|
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(
|
const elClass = customElements.get(
|
||||||
`ha-automation-condition-${type}`
|
`ha-automation-condition-${type}`
|
||||||
) as CustomElementConstructor & {
|
) as CustomElementConstructor & {
|
||||||
|
|||||||
@@ -56,12 +56,19 @@ import {
|
|||||||
type AutomationElementGroup,
|
type AutomationElementGroup,
|
||||||
type AutomationElementGroupCollection,
|
type AutomationElementGroupCollection,
|
||||||
} from "../../../data/automation";
|
} from "../../../data/automation";
|
||||||
|
import type { ConditionDescriptions } from "../../../data/condition";
|
||||||
import {
|
import {
|
||||||
CONDITION_BUILDING_BLOCKS_GROUP,
|
CONDITION_BUILDING_BLOCKS_GROUP,
|
||||||
CONDITION_COLLECTIONS,
|
CONDITION_COLLECTIONS,
|
||||||
CONDITION_ICONS,
|
getConditionDomain,
|
||||||
|
getConditionObjectId,
|
||||||
|
subscribeConditions,
|
||||||
} from "../../../data/condition";
|
} from "../../../data/condition";
|
||||||
import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
|
import {
|
||||||
|
getConditionIcons,
|
||||||
|
getServiceIcons,
|
||||||
|
getTriggerIcons,
|
||||||
|
} from "../../../data/icons";
|
||||||
import type { IntegrationManifest } from "../../../data/integration";
|
import type { IntegrationManifest } from "../../../data/integration";
|
||||||
import {
|
import {
|
||||||
domainToName,
|
domainToName,
|
||||||
@@ -82,6 +89,7 @@ import { isMac } from "../../../util/is_mac";
|
|||||||
import { showToast } from "../../../util/toast";
|
import { showToast } from "../../../util/toast";
|
||||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||||
import { PASTE_VALUE } 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 = {
|
const TYPES = {
|
||||||
trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS },
|
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 ENTITY_DOMAINS_MAIN = new Set(["notify"]);
|
||||||
|
|
||||||
const ACTION_SERVICE_KEYWORDS = ["dynamicGroups", "helpers", "other"];
|
const DYNAMIC_KEYWORDS = ["dynamicGroups", "helpers", "other"];
|
||||||
|
|
||||||
@customElement("add-automation-element-dialog")
|
@customElement("add-automation-element-dialog")
|
||||||
class DialogAddAutomationElement
|
class DialogAddAutomationElement
|
||||||
@@ -152,6 +160,8 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
||||||
|
|
||||||
|
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||||
|
|
||||||
@query(".items ha-md-list ha-md-list-item")
|
@query(".items ha-md-list ha-md-list-item")
|
||||||
private _itemsListFirstElement?: HaMdList;
|
private _itemsListFirstElement?: HaMdList;
|
||||||
|
|
||||||
@@ -169,15 +179,15 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
this.addKeyboardShortcuts();
|
this.addKeyboardShortcuts();
|
||||||
|
|
||||||
|
this._unsubscribe();
|
||||||
|
this._fetchManifests();
|
||||||
|
|
||||||
if (this._params?.type === "action") {
|
if (this._params?.type === "action") {
|
||||||
this.hass.loadBackendTranslation("services");
|
this.hass.loadBackendTranslation("services");
|
||||||
this._fetchManifests();
|
|
||||||
this._calculateUsedDomains();
|
this._calculateUsedDomains();
|
||||||
getServiceIcons(this.hass);
|
getServiceIcons(this.hass);
|
||||||
}
|
} else if (this._params?.type === "trigger") {
|
||||||
if (this._params?.type === "trigger") {
|
|
||||||
this.hass.loadBackendTranslation("triggers");
|
this.hass.loadBackendTranslation("triggers");
|
||||||
this._fetchManifests();
|
|
||||||
getTriggerIcons(this.hass);
|
getTriggerIcons(this.hass);
|
||||||
this._unsub = subscribeTriggers(this.hass, (triggers) => {
|
this._unsub = subscribeTriggers(this.hass, (triggers) => {
|
||||||
this._triggerDescriptions = {
|
this._triggerDescriptions = {
|
||||||
@@ -185,7 +195,17 @@ class DialogAddAutomationElement
|
|||||||
...triggers,
|
...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(
|
this._fullScreen = matchMedia(
|
||||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
).matches;
|
).matches;
|
||||||
@@ -199,10 +219,7 @@ class DialogAddAutomationElement
|
|||||||
|
|
||||||
public closeDialog() {
|
public closeDialog() {
|
||||||
this.removeKeyboardShortcuts();
|
this.removeKeyboardShortcuts();
|
||||||
if (this._unsub) {
|
this._unsubscribe();
|
||||||
this._unsub.then((unsub) => unsub());
|
|
||||||
this._unsub = undefined;
|
|
||||||
}
|
|
||||||
if (this._params) {
|
if (this._params) {
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
}
|
}
|
||||||
@@ -219,6 +236,13 @@ class DialogAddAutomationElement
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _unsubscribe() {
|
||||||
|
if (this._unsub) {
|
||||||
|
this._unsub.then((unsub) => unsub());
|
||||||
|
this._unsub = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private _getGroups = (
|
private _getGroups = (
|
||||||
type: AddAutomationElementDialogParams["type"],
|
type: AddAutomationElementDialogParams["type"],
|
||||||
group?: string,
|
group?: string,
|
||||||
@@ -348,8 +372,11 @@ class DialogAddAutomationElement
|
|||||||
items.push(
|
items.push(
|
||||||
...this._triggers(localize, this._triggerDescriptions, manifests)
|
...this._triggers(localize, this._triggerDescriptions, manifests)
|
||||||
);
|
);
|
||||||
}
|
} else if (type === "condition") {
|
||||||
if (type === "action") {
|
items.push(
|
||||||
|
...this._conditions(localize, this._conditionDescriptions, manifests)
|
||||||
|
);
|
||||||
|
} else if (type === "action") {
|
||||||
items.push(...this._services(localize, services, manifests));
|
items.push(...this._services(localize, services, manifests));
|
||||||
}
|
}
|
||||||
return items;
|
return items;
|
||||||
@@ -372,6 +399,7 @@ class DialogAddAutomationElement
|
|||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
services: HomeAssistant["services"],
|
services: HomeAssistant["services"],
|
||||||
triggerDescriptions: TriggerDescriptions,
|
triggerDescriptions: TriggerDescriptions,
|
||||||
|
conditionDescriptions: ConditionDescriptions,
|
||||||
manifests?: DomainManifestLookup
|
manifests?: DomainManifestLookup
|
||||||
): {
|
): {
|
||||||
titleKey?: LocalizeKeys;
|
titleKey?: LocalizeKeys;
|
||||||
@@ -383,35 +411,10 @@ class DialogAddAutomationElement
|
|||||||
let collectionGroups = Object.entries(collection.groups);
|
let collectionGroups = Object.entries(collection.groups);
|
||||||
const groups: ListItem[] = [];
|
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 (
|
if (
|
||||||
type === "trigger" &&
|
type === "trigger" &&
|
||||||
Object.keys(collection.groups).some((item) =>
|
Object.keys(collection.groups).some((item) =>
|
||||||
ACTION_SERVICE_KEYWORDS.includes(item)
|
DYNAMIC_KEYWORDS.includes(item)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
groups.push(
|
groups.push(
|
||||||
@@ -429,7 +432,53 @@ class DialogAddAutomationElement
|
|||||||
);
|
);
|
||||||
|
|
||||||
collectionGroups = collectionGroups.filter(
|
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"],
|
services: HomeAssistant["services"],
|
||||||
manifests?: DomainManifestLookup
|
manifests?: DomainManifestLookup
|
||||||
): ListItem[] => {
|
): ListItem[] => {
|
||||||
if (type === "action" && isDynamic(group)) {
|
|
||||||
return this._services(localize, services, manifests, group);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type === "trigger" && isDynamic(group)) {
|
if (type === "trigger" && isDynamic(group)) {
|
||||||
return this._triggers(
|
return this._triggers(
|
||||||
localize,
|
localize,
|
||||||
@@ -499,6 +544,17 @@ class DialogAddAutomationElement
|
|||||||
group
|
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);
|
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(
|
private _services = memoizeOne(
|
||||||
(
|
(
|
||||||
localize: LocalizeFunc,
|
localize: LocalizeFunc,
|
||||||
@@ -832,6 +984,7 @@ class DialogAddAutomationElement
|
|||||||
this.hass.localize,
|
this.hass.localize,
|
||||||
this.hass.services,
|
this.hass.services,
|
||||||
this._triggerDescriptions,
|
this._triggerDescriptions,
|
||||||
|
this._conditionDescriptions,
|
||||||
this._manifests
|
this._manifests
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -1136,6 +1289,7 @@ class DialogAddAutomationElement
|
|||||||
super.disconnectedCallback();
|
super.disconnectedCallback();
|
||||||
window.removeEventListener("resize", this._updateNarrow);
|
window.removeEventListener("resize", this._updateNarrow);
|
||||||
this._removeSearchKeybindings();
|
this._removeSearchKeybindings();
|
||||||
|
this._unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
private _updateNarrow = () => {
|
private _updateNarrow = () => {
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ import "../../../../components/ha-yaml-editor";
|
|||||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||||
import type { Condition } from "../../../../data/automation";
|
import type { Condition } from "../../../../data/automation";
|
||||||
import { expandConditionWithShorthand } from "../../../../data/automation";
|
import { expandConditionWithShorthand } from "../../../../data/automation";
|
||||||
|
import type { ConditionDescription } from "../../../../data/condition";
|
||||||
import { COLLAPSIBLE_CONDITION_ELEMENTS } from "../../../../data/condition";
|
import { COLLAPSIBLE_CONDITION_ELEMENTS } from "../../../../data/condition";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import "../ha-automation-editor-warning";
|
import "../ha-automation-editor-warning";
|
||||||
import { editorStyles, indentStyle } from "../styles";
|
import { editorStyles, indentStyle } from "../styles";
|
||||||
import type { ConditionElement } from "./ha-automation-condition-row";
|
import type { ConditionElement } from "./ha-automation-condition-row";
|
||||||
|
import "./types/ha-automation-condition-platform";
|
||||||
|
|
||||||
@customElement("ha-automation-condition-editor")
|
@customElement("ha-automation-condition-editor")
|
||||||
export default class HaAutomationConditionEditor extends LitElement {
|
export default class HaAutomationConditionEditor extends LitElement {
|
||||||
@@ -35,6 +37,8 @@ export default class HaAutomationConditionEditor extends LitElement {
|
|||||||
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
|
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
|
||||||
false;
|
false;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public description?: ConditionDescription;
|
||||||
|
|
||||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||||
|
|
||||||
@query(COLLAPSIBLE_CONDITION_ELEMENTS.join(", "))
|
@query(COLLAPSIBLE_CONDITION_ELEMENTS.join(", "))
|
||||||
@@ -83,16 +87,23 @@ export default class HaAutomationConditionEditor extends LitElement {
|
|||||||
`
|
`
|
||||||
: html`
|
: html`
|
||||||
<div @value-changed=${this._onUiChanged}>
|
<div @value-changed=${this._onUiChanged}>
|
||||||
${dynamicElement(
|
${this.description
|
||||||
`ha-automation-condition-${condition.condition}`,
|
? html`<ha-automation-condition-platform
|
||||||
{
|
.hass=${this.hass}
|
||||||
hass: this.hass,
|
.condition=${this.condition}
|
||||||
condition: condition,
|
.description=${this.description}
|
||||||
disabled: this.disabled,
|
.disabled=${this.disabled}
|
||||||
optionsInSidebar: this.indent,
|
></ha-automation-condition-platform>`
|
||||||
narrow: this.narrow,
|
: dynamicElement(
|
||||||
}
|
`ha-automation-condition-${condition.condition}`,
|
||||||
)}
|
{
|
||||||
|
hass: this.hass,
|
||||||
|
condition: condition,
|
||||||
|
disabled: this.disabled,
|
||||||
|
optionsInSidebar: this.indent,
|
||||||
|
narrow: this.narrow,
|
||||||
|
}
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
|||||||
import "../../../../components/ha-automation-row";
|
import "../../../../components/ha-automation-row";
|
||||||
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
|
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-condition-icon";
|
||||||
import "../../../../components/ha-expansion-panel";
|
import "../../../../components/ha-expansion-panel";
|
||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import "../../../../components/ha-md-button-menu";
|
import "../../../../components/ha-md-button-menu";
|
||||||
@@ -44,10 +45,8 @@ import type {
|
|||||||
} from "../../../../data/automation";
|
} from "../../../../data/automation";
|
||||||
import { isCondition, testCondition } from "../../../../data/automation";
|
import { isCondition, testCondition } from "../../../../data/automation";
|
||||||
import { describeCondition } from "../../../../data/automation_i18n";
|
import { describeCondition } from "../../../../data/automation_i18n";
|
||||||
import {
|
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||||
CONDITION_BUILDING_BLOCKS,
|
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||||
CONDITION_ICONS,
|
|
||||||
} from "../../../../data/condition";
|
|
||||||
import { validateConfig } from "../../../../data/config";
|
import { validateConfig } from "../../../../data/config";
|
||||||
import { fullEntitiesContext } from "../../../../data/context";
|
import { fullEntitiesContext } from "../../../../data/context";
|
||||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||||
@@ -130,6 +129,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
|
|
||||||
@state() private _warnings?: string[];
|
@state() private _warnings?: string[];
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public conditionDescriptions: ConditionDescriptions = {};
|
||||||
|
|
||||||
@property({ type: Boolean, attribute: "sidebar" })
|
@property({ type: Boolean, attribute: "sidebar" })
|
||||||
public optionsInSidebar = false;
|
public optionsInSidebar = false;
|
||||||
|
|
||||||
@@ -179,11 +181,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
|
|
||||||
private _renderRow() {
|
private _renderRow() {
|
||||||
return html`
|
return html`
|
||||||
<ha-svg-icon
|
<ha-condition-icon
|
||||||
slot="leading-icon"
|
slot="leading-icon"
|
||||||
class="condition-icon"
|
.hass=${this.hass}
|
||||||
.path=${CONDITION_ICONS[this.condition.condition]}
|
.condition=${this.condition.condition}
|
||||||
></ha-svg-icon>
|
></ha-condition-icon>
|
||||||
<h3 slot="header">
|
<h3 slot="header">
|
||||||
${capitalizeFirstLetter(
|
${capitalizeFirstLetter(
|
||||||
describeCondition(this.condition, this.hass, this._entityReg)
|
describeCondition(this.condition, this.hass, this._entityReg)
|
||||||
@@ -395,9 +397,14 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
<ha-automation-condition-editor
|
<ha-automation-condition-editor
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.condition=${this.condition}
|
.condition=${this.condition}
|
||||||
|
.description=${this.conditionDescriptions[
|
||||||
|
this.condition.condition
|
||||||
|
]}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.yamlMode=${this._yamlMode}
|
.yamlMode=${this._yamlMode}
|
||||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
.uiSupported=${this._uiSupported(
|
||||||
|
this._getType(this.condition, this.conditionDescriptions)
|
||||||
|
)}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||||
></ha-automation-condition-editor>`
|
></ha-automation-condition-editor>`
|
||||||
@@ -476,7 +483,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.condition=${this.condition}
|
.condition=${this.condition}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
.uiSupported=${this._uiSupported(
|
||||||
|
this._getType(this.condition, this.conditionDescriptions)
|
||||||
|
)}
|
||||||
indent
|
indent
|
||||||
.selected=${this._selected}
|
.selected=${this._selected}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
@@ -786,7 +795,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
|||||||
cut: this._cutCondition,
|
cut: this._cutCondition,
|
||||||
test: this._testCondition,
|
test: this._testCondition,
|
||||||
config: sidebarCondition,
|
config: sidebarCondition,
|
||||||
uiSupported: this._uiSupported(sidebarCondition.condition),
|
uiSupported: this._uiSupported(
|
||||||
|
this._getType(sidebarCondition, this.conditionDescriptions)
|
||||||
|
),
|
||||||
|
description: this.conditionDescriptions[sidebarCondition.condition],
|
||||||
yamlMode: this._yamlMode,
|
yamlMode: this._yamlMode,
|
||||||
} satisfies ConditionSidebarConfig);
|
} satisfies ConditionSidebarConfig);
|
||||||
this._selected = true;
|
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(
|
private _uiSupported = memoizeOne(
|
||||||
(type: string) =>
|
(type: string) =>
|
||||||
customElements.get(`ha-automation-condition-${type}`) !== undefined
|
customElements.get(`ha-automation-condition-${type}`) !== undefined
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { PropertyValues } from "lit";
|
|||||||
import { html, LitElement, nothing } from "lit";
|
import { html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||||
@@ -12,11 +13,18 @@ import "../../../../components/ha-button";
|
|||||||
import "../../../../components/ha-button-menu";
|
import "../../../../components/ha-button-menu";
|
||||||
import "../../../../components/ha-sortable";
|
import "../../../../components/ha-sortable";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import type {
|
import {
|
||||||
AutomationClipboard,
|
getValueFromDynamic,
|
||||||
Condition,
|
isDynamic,
|
||||||
|
type AutomationClipboard,
|
||||||
|
type Condition,
|
||||||
} from "../../../../data/automation";
|
} 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 type { HomeAssistant } from "../../../../types";
|
||||||
import {
|
import {
|
||||||
PASTE_VALUE,
|
PASTE_VALUE,
|
||||||
@@ -25,10 +33,9 @@ import {
|
|||||||
import { automationRowsStyles } from "../styles";
|
import { automationRowsStyles } from "../styles";
|
||||||
import "./ha-automation-condition-row";
|
import "./ha-automation-condition-row";
|
||||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
|
||||||
|
|
||||||
@customElement("ha-automation-condition")
|
@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 hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public conditions!: Condition[];
|
@property({ attribute: false }) public conditions!: Condition[];
|
||||||
@@ -46,6 +53,8 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
|
|
||||||
@state() private _rowSortSelected?: number;
|
@state() private _rowSortSelected?: number;
|
||||||
|
|
||||||
|
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||||
|
|
||||||
@state()
|
@state()
|
||||||
@storage({
|
@storage({
|
||||||
key: "automationClipboard",
|
key: "automationClipboard",
|
||||||
@@ -64,6 +73,26 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
|
|
||||||
private _conditionKeys = new WeakMap<Condition, string>();
|
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) {
|
protected updated(changedProperties: PropertyValues) {
|
||||||
if (!changedProperties.has("conditions")) {
|
if (!changedProperties.has("conditions")) {
|
||||||
return;
|
return;
|
||||||
@@ -168,6 +197,7 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
.last=${idx === this.conditions.length - 1}
|
.last=${idx === this.conditions.length - 1}
|
||||||
.totalConditions=${this.conditions.length}
|
.totalConditions=${this.conditions.length}
|
||||||
.condition=${cond}
|
.condition=${cond}
|
||||||
|
.conditionDescriptions=${this._conditionDescriptions}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
@duplicate=${this._duplicateCondition}
|
@duplicate=${this._duplicateCondition}
|
||||||
@@ -237,6 +267,10 @@ export default class HaAutomationCondition extends LitElement {
|
|||||||
conditions = this.conditions.concat(
|
conditions = this.conditions.concat(
|
||||||
deepClone(this._clipboard!.condition)
|
deepClone(this._clipboard!.condition)
|
||||||
);
|
);
|
||||||
|
} else if (isDynamic(value)) {
|
||||||
|
conditions = this.conditions.concat({
|
||||||
|
condition: getValueFromDynamic(value),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
const condition = value as Condition["condition"];
|
const condition = value as Condition["condition"];
|
||||||
const elClass = customElements.get(
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,11 +16,16 @@ import { classMap } from "lit/directives/class-map";
|
|||||||
import { keyed } from "lit/directives/keyed";
|
import { keyed } from "lit/directives/keyed";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||||
import {
|
import type {
|
||||||
testCondition,
|
LegacyCondition,
|
||||||
type ConditionSidebarConfig,
|
ConditionSidebarConfig,
|
||||||
} from "../../../../data/automation";
|
} 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 { validateConfig } from "../../../../data/config";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { isMac } from "../../../../util/is_mac";
|
import { isMac } from "../../../../util/is_mac";
|
||||||
@@ -84,14 +89,25 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
|||||||
"ui.panel.config.automation.editor.conditions.condition"
|
"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 =
|
const title =
|
||||||
this.hass.localize(
|
this.hass.localize(
|
||||||
`ui.panel.config.automation.editor.conditions.type.${type}.label`
|
`ui.panel.config.automation.editor.conditions.type.${type as LegacyCondition["condition"]}.label`
|
||||||
) || type;
|
) ||
|
||||||
|
this.hass.localize(
|
||||||
|
`component.${domain}.conditions.${conditionName}.name`
|
||||||
|
) ||
|
||||||
|
type;
|
||||||
|
|
||||||
const description = isBuildingBlock
|
const description = isBuildingBlock
|
||||||
? this.hass.localize(
|
? 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"
|
class="sidebar-editor"
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.condition=${this.config.config}
|
.condition=${this.config.config}
|
||||||
|
.description=${this.config.description}
|
||||||
.yamlMode=${this.yamlMode}
|
.yamlMode=${this.yamlMode}
|
||||||
.uiSupported=${this.config.uiSupported}
|
.uiSupported=${this.config.uiSupported}
|
||||||
@value-changed=${this._valueChangedSidebar}
|
@value-changed=${this._valueChangedSidebar}
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { mdiOpenInNew } from "@mdi/js";
|
|
||||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
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 { customElement, property, state } from "lit/decorators";
|
||||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
import "../../../components/ha-analytics";
|
import "../../../components/ha-analytics";
|
||||||
import "../../../components/ha-button";
|
|
||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-checkbox";
|
|
||||||
import "../../../components/ha-settings-row";
|
import "../../../components/ha-settings-row";
|
||||||
import "../../../components/ha-svg-icon";
|
|
||||||
import type { Analytics } from "../../../data/analytics";
|
import type { Analytics } from "../../../data/analytics";
|
||||||
import {
|
import {
|
||||||
getAnalyticsDetails,
|
getAnalyticsDetails,
|
||||||
@@ -17,6 +13,8 @@ import {
|
|||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../types";
|
import type { HomeAssistant } from "../../../types";
|
||||||
import { documentationUrl } from "../../../util/documentation-url";
|
import { documentationUrl } from "../../../util/documentation-url";
|
||||||
|
import type { HaSwitch } from "../../../components/ha-switch";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
|
||||||
@customElement("ha-config-analytics")
|
@customElement("ha-config-analytics")
|
||||||
class ConfigAnalytics extends LitElement {
|
class ConfigAnalytics extends LitElement {
|
||||||
@@ -34,10 +32,22 @@ class ConfigAnalytics extends LitElement {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card outlined>
|
<ha-card
|
||||||
|
outlined
|
||||||
|
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
|
||||||
|
"Home Assistant analytics"}
|
||||||
|
>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
${error ? html`<div class="error">${error}</div>` : ""}
|
${error ? html`<div class="error">${error}</div>` : nothing}
|
||||||
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
|
<p>
|
||||||
|
${this.hass.localize("ui.panel.config.analytics.intro")}
|
||||||
|
<a
|
||||||
|
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>${this.hass.localize("ui.panel.config.analytics.learn_more")}</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
<ha-analytics
|
<ha-analytics
|
||||||
translation_key_panel="config"
|
translation_key_panel="config"
|
||||||
@analytics-preferences-changed=${this._preferencesChanged}
|
@analytics-preferences-changed=${this._preferencesChanged}
|
||||||
@@ -45,26 +55,59 @@ class ConfigAnalytics extends LitElement {
|
|||||||
.analytics=${this._analyticsDetails}
|
.analytics=${this._analyticsDetails}
|
||||||
></ha-analytics>
|
></ha-analytics>
|
||||||
</div>
|
</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>
|
</ha-card>
|
||||||
<div class="footer">
|
${this._analyticsDetails &&
|
||||||
<ha-button
|
"snapshots" in this._analyticsDetails.preferences
|
||||||
size="small"
|
? html`<ha-card
|
||||||
appearance="plain"
|
outlined
|
||||||
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
.header=${this.hass.localize(
|
||||||
target="_blank"
|
"ui.panel.config.analytics.preferences.snapshots.header"
|
||||||
rel="noreferrer"
|
)}
|
||||||
>
|
>
|
||||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
<div class="card-content">
|
||||||
${this.hass.localize("ui.panel.config.analytics.learn_more")}
|
<p>
|
||||||
</ha-button>
|
${this.hass.localize(
|
||||||
</div>
|
"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 {
|
private _preferencesChanged(event: CustomEvent): void {
|
||||||
this._analyticsDetails = {
|
this._analyticsDetails = {
|
||||||
...this._analyticsDetails!,
|
...this._analyticsDetails!,
|
||||||
preferences: event.detail.preferences,
|
preferences: event.detail.preferences,
|
||||||
};
|
};
|
||||||
|
this._save();
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
@@ -117,21 +174,10 @@ class ConfigAnalytics extends LitElement {
|
|||||||
p {
|
p {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
.card-actions {
|
ha-card:not(:first-of-type) {
|
||||||
display: flex;
|
margin-top: 24px;
|
||||||
flex-direction: row-reverse;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
.footer {
|
`,
|
||||||
padding: 32px 0 16px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
ha-button[size="small"] ha-svg-icon {
|
|
||||||
--mdc-icon-size: 16px;
|
|
||||||
}
|
|
||||||
`, // row-reverse so we tab first to "save"
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
||||||
|
import { stringCompare } from "../../../../common/string/compare";
|
||||||
import { titleCase } from "../../../../common/string/title-case";
|
import { titleCase } from "../../../../common/string/title-case";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
import type { DeviceRegistryEntry } from "../../../../data/device_registry";
|
import type { DeviceRegistryEntry } from "../../../../data/device_registry";
|
||||||
@@ -9,16 +11,61 @@ import { haStyle } from "../../../../resources/styles";
|
|||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { createSearchParam } from "../../../../common/url/search-params";
|
import { createSearchParam } from "../../../../common/url/search-params";
|
||||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||||
|
import "../../../../components/ha-icon";
|
||||||
|
import "../../../../components/ha-label";
|
||||||
|
import type { LabelRegistryEntry } from "../../../../data/label_registry";
|
||||||
|
import { subscribeLabelRegistry } from "../../../../data/label_registry";
|
||||||
|
import { computeCssColor } from "../../../../common/color/compute-color";
|
||||||
|
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||||
|
|
||||||
@customElement("ha-device-info-card")
|
@customElement("ha-device-info-card")
|
||||||
export class HaDeviceCard extends LitElement {
|
export class HaDeviceCard extends SubscribeMixin(LitElement) {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow = false;
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
|
@state() private _labelRegistry?: LabelRegistryEntry[];
|
||||||
|
|
||||||
|
private _labelsData = memoizeOne(
|
||||||
|
(
|
||||||
|
labels: LabelRegistryEntry[] | undefined,
|
||||||
|
labelIds: string[],
|
||||||
|
language: string
|
||||||
|
): {
|
||||||
|
map: Map<string, LabelRegistryEntry>;
|
||||||
|
ids: string[];
|
||||||
|
} => {
|
||||||
|
const map = labels
|
||||||
|
? new Map(labels.map((label) => [label.label_id, label]))
|
||||||
|
: new Map<string, LabelRegistryEntry>();
|
||||||
|
const ids = [...labelIds].sort((labelA, labelB) =>
|
||||||
|
stringCompare(
|
||||||
|
map.get(labelA)?.name || labelA,
|
||||||
|
map.get(labelB)?.name || labelB,
|
||||||
|
language
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return { map, ids };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public hassSubscribe() {
|
||||||
|
return [
|
||||||
|
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||||
|
this._labelRegistry = labels;
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
const { map: labelMap, ids: labels } = this._labelsData(
|
||||||
|
this._labelRegistry,
|
||||||
|
this.device.labels,
|
||||||
|
this.hass.locale.language
|
||||||
|
);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card
|
<ha-card
|
||||||
outlined
|
outlined
|
||||||
@@ -58,7 +105,7 @@ export class HaDeviceCard extends LitElement {
|
|||||||
<span class="hub"
|
<span class="hub"
|
||||||
><a
|
><a
|
||||||
href="/config/devices/device/${this.device.via_device_id}"
|
href="/config/devices/device/${this.device.via_device_id}"
|
||||||
>${this._computeDeviceNameDislay(
|
>${this._computeDeviceNameDisplay(
|
||||||
this.device.via_device_id
|
this.device.via_device_id
|
||||||
)}</a
|
)}</a
|
||||||
></span
|
></span
|
||||||
@@ -126,6 +173,34 @@ export class HaDeviceCard extends LitElement {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
|
${labels.length > 0
|
||||||
|
? html`
|
||||||
|
<div class="extra-info labels">
|
||||||
|
${labels.map((labelId) => {
|
||||||
|
const label = labelMap.get(labelId);
|
||||||
|
const color =
|
||||||
|
label?.color && typeof label.color === "string"
|
||||||
|
? computeCssColor(label.color)
|
||||||
|
: undefined;
|
||||||
|
return html`
|
||||||
|
<ha-label
|
||||||
|
style=${color ? `--color: ${color}` : ""}
|
||||||
|
.description=${label?.description}
|
||||||
|
>
|
||||||
|
${label?.icon
|
||||||
|
? html`<ha-icon
|
||||||
|
slot="icon"
|
||||||
|
.icon=${label.icon}
|
||||||
|
></ha-icon>`
|
||||||
|
: nothing}
|
||||||
|
${label?.name || labelId}
|
||||||
|
</ha-label>
|
||||||
|
`;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
|
||||||
<slot></slot>
|
<slot></slot>
|
||||||
</div>
|
</div>
|
||||||
<slot name="actions"></slot>
|
<slot name="actions"></slot>
|
||||||
@@ -139,7 +214,7 @@ export class HaDeviceCard extends LitElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _computeDeviceNameDislay(deviceId) {
|
private _computeDeviceNameDisplay(deviceId: string) {
|
||||||
const device = this.hass.devices[deviceId];
|
const device = this.hass.devices[deviceId];
|
||||||
return device
|
return device
|
||||||
? computeDeviceNameDisplay(device, this.hass)
|
? computeDeviceNameDisplay(device, this.hass)
|
||||||
@@ -162,8 +237,26 @@ export class HaDeviceCard extends LitElement {
|
|||||||
.device {
|
.device {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
}
|
}
|
||||||
|
.labels {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--ha-space-1);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.labels ha-label {
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
flex: 0 1 auto;
|
||||||
|
}
|
||||||
|
ha-label {
|
||||||
|
--ha-label-background-color: var(--color, var(--grey-color));
|
||||||
|
--ha-label-background-opacity: 0.5;
|
||||||
|
--ha-label-text-color: var(--primary-text-color);
|
||||||
|
--ha-label-icon-color: var(--primary-text-color);
|
||||||
|
}
|
||||||
.extra-info {
|
.extra-info {
|
||||||
margin-top: 8px;
|
margin-top: var(--ha-space-2);
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
.manuf,
|
.manuf,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import "../../../lovelace/editor/dashboard-strategy-editor/hui-dashboard-strateg
|
|||||||
import type { LovelaceDashboardConfigureStrategyDialogParams } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
import type { LovelaceDashboardConfigureStrategyDialogParams } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||||
|
|
||||||
@customElement("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;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
@state() private _params?: LovelaceDashboardConfigureStrategyDialogParams;
|
@state() private _params?: LovelaceDashboardConfigureStrategyDialogParams;
|
||||||
@@ -97,6 +97,6 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
|||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
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 { createCloseHeading } from "../../../../components/ha-dialog";
|
||||||
import "../../../../components/ha-form/ha-form";
|
import "../../../../components/ha-form/ha-form";
|
||||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
|
||||||
import type {
|
import type {
|
||||||
LovelaceDashboard,
|
LovelaceDashboard,
|
||||||
LovelaceDashboardCreateParams,
|
LovelaceDashboardCreateParams,
|
||||||
LovelaceDashboardMutableParams,
|
LovelaceDashboardMutableParams,
|
||||||
} from "../../../../data/lovelace/dashboard";
|
} from "../../../../data/lovelace/dashboard";
|
||||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
|
||||||
import { haStyleDialog } from "../../../../resources/styles";
|
import { haStyleDialog } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
|
|
||||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||||
|
|
||||||
@customElement("dialog-lovelace-dashboard-detail")
|
@customElement("dialog-lovelace-dashboard-detail")
|
||||||
@@ -61,9 +58,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
|||||||
if (!this._params || !this._data) {
|
if (!this._params || !this._data) {
|
||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
const defaultPanelUrlPath =
|
|
||||||
this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
|
||||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||||
|
const isLovelaceDashboard = this._params.urlPath === "lovelace";
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-dialog
|
<ha-dialog
|
||||||
@@ -88,9 +85,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
|||||||
? this.hass.localize(
|
? this.hass.localize(
|
||||||
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
||||||
)
|
)
|
||||||
: this._params.urlPath === "lovelace"
|
: isLovelaceDashboard
|
||||||
? this.hass.localize(
|
? this.hass.localize(
|
||||||
"ui.panel.config.lovelace.dashboards.cant_edit_default"
|
"ui.panel.config.lovelace.dashboards.cant_edit_lovelace"
|
||||||
)
|
)
|
||||||
: html`
|
: html`
|
||||||
<ha-form
|
<ha-form
|
||||||
@@ -119,24 +116,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</ha-button>
|
</ha-button>
|
||||||
`
|
`
|
||||||
: ""}
|
: nothing}
|
||||||
<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}
|
||||||
<ha-button
|
<ha-button
|
||||||
slot="primaryAction"
|
slot="primaryAction"
|
||||||
@click=${this._updateDashboard}
|
@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() {
|
private async _updateDashboard() {
|
||||||
if (this._params?.urlPath && !this._params.dashboard?.id) {
|
if (this._params?.urlPath && !this._params.dashboard?.id) {
|
||||||
this.closeDialog();
|
this.closeDialog();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
mdiCheck,
|
mdiCheck,
|
||||||
mdiCheckCircleOutline,
|
|
||||||
mdiDelete,
|
mdiDelete,
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
|
mdiHomeCircleOutline,
|
||||||
|
mdiHomeEdit,
|
||||||
mdiPencil,
|
mdiPencil,
|
||||||
mdiPlus,
|
mdiPlus,
|
||||||
} from "@mdi/js";
|
} from "@mdi/js";
|
||||||
@@ -10,7 +11,6 @@ import type { PropertyValues } from "lit";
|
|||||||
import { LitElement, html, nothing } from "lit";
|
import { LitElement, html, nothing } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoize from "memoize-one";
|
import memoize from "memoize-one";
|
||||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
|
||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { navigate } from "../../../../common/navigate";
|
import { navigate } from "../../../../common/navigate";
|
||||||
import { stringCompare } from "../../../../common/string/compare";
|
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-md-list-item";
|
||||||
import "../../../../components/ha-svg-icon";
|
import "../../../../components/ha-svg-icon";
|
||||||
import "../../../../components/ha-tooltip";
|
import "../../../../components/ha-tooltip";
|
||||||
|
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||||
import type { LovelacePanelConfig } from "../../../../data/lovelace";
|
import type { LovelacePanelConfig } from "../../../../data/lovelace";
|
||||||
import type { LovelaceRawConfig } from "../../../../data/lovelace/config/types";
|
import type { LovelaceRawConfig } from "../../../../data/lovelace/config/types";
|
||||||
import {
|
import {
|
||||||
@@ -45,7 +46,11 @@ import {
|
|||||||
fetchDashboards,
|
fetchDashboards,
|
||||||
updateDashboard,
|
updateDashboard,
|
||||||
} from "../../../../data/lovelace/dashboard";
|
} 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 { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||||
import "../../../../layouts/hass-loading-screen";
|
import "../../../../layouts/hass-loading-screen";
|
||||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
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 { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||||
|
|
||||||
|
export const PANEL_DASHBOARDS = [
|
||||||
|
"home",
|
||||||
|
"light",
|
||||||
|
"security",
|
||||||
|
"climate",
|
||||||
|
"energy",
|
||||||
|
] as string[];
|
||||||
|
|
||||||
type DataTableItem = Pick<
|
type DataTableItem = Pick<
|
||||||
LovelaceDashboard,
|
LovelaceDashboard,
|
||||||
"icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path"
|
"icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path"
|
||||||
> & {
|
> & {
|
||||||
default: boolean;
|
default: boolean;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
localized_type: string;
|
||||||
type: string;
|
type: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -112,7 +126,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
state: false,
|
state: false,
|
||||||
subscribe: false,
|
subscribe: false,
|
||||||
})
|
})
|
||||||
private _activeGrouping?: string = "type";
|
private _activeGrouping?: string = "localized_type";
|
||||||
|
|
||||||
@storage({
|
@storage({
|
||||||
key: "lovelace-dashboards-table-collapsed",
|
key: "lovelace-dashboards-table-collapsed",
|
||||||
@@ -167,7 +181,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
<ha-svg-icon
|
<ha-svg-icon
|
||||||
.id="default-icon-${dashboard.title}"
|
.id="default-icon-${dashboard.title}"
|
||||||
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
|
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
|
||||||
.path=${mdiCheckCircleOutline}
|
.path=${mdiHomeCircleOutline}
|
||||||
></ha-svg-icon>
|
></ha-svg-icon>
|
||||||
<ha-tooltip
|
<ha-tooltip
|
||||||
.for="default-icon-${dashboard.title}"
|
.for="default-icon-${dashboard.title}"
|
||||||
@@ -183,7 +197,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
columns.type = {
|
columns.localized_type = {
|
||||||
title: localize(
|
title: localize(
|
||||||
"ui.panel.config.lovelace.dashboards.picker.headers.type"
|
"ui.panel.config.lovelace.dashboards.picker.headers.type"
|
||||||
),
|
),
|
||||||
@@ -253,7 +267,15 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
narrow
|
narrow
|
||||||
.items=${[
|
.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,
|
path: mdiPencil,
|
||||||
@@ -262,10 +284,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
),
|
),
|
||||||
action: () => this._handleEdit(dashboard),
|
action: () => this._handleEdit(dashboard),
|
||||||
},
|
},
|
||||||
]
|
|
||||||
: []),
|
|
||||||
...(this._canDelete(dashboard.url_path)
|
|
||||||
? [
|
|
||||||
{
|
{
|
||||||
label: this.hass.localize(
|
label: this.hass.localize(
|
||||||
"ui.panel.config.lovelace.dashboards.picker.delete"
|
"ui.panel.config.lovelace.dashboards.picker.delete"
|
||||||
@@ -288,92 +306,43 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
|
|
||||||
private _getItems = memoize(
|
private _getItems = memoize(
|
||||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
|
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
|
||||||
const defaultMode = (
|
const mode = (this.hass.panels?.lovelace?.config as LovelacePanelConfig)
|
||||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
.mode;
|
||||||
).mode;
|
|
||||||
const isDefault = defaultUrlPath === "lovelace";
|
const isDefault = defaultUrlPath === "lovelace";
|
||||||
const result: DataTableItem[] = [
|
const result: DataTableItem[] = [
|
||||||
{
|
{
|
||||||
icon: "mdi:view-dashboard",
|
icon: "mdi:view-dashboard",
|
||||||
title: this.hass.localize("panel.states"),
|
title: this.hass.localize("panel.states"),
|
||||||
default: isDefault,
|
default: isDefault,
|
||||||
show_in_sidebar: isDefault,
|
show_in_sidebar: true,
|
||||||
require_admin: false,
|
require_admin: false,
|
||||||
url_path: "lovelace",
|
url_path: "lovelace",
|
||||||
mode: defaultMode,
|
mode: mode,
|
||||||
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
|
filename: mode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||||
type: this._localizeType("built_in"),
|
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) {
|
PANEL_DASHBOARDS.forEach((panel) => {
|
||||||
result.push({
|
const panelInfo = this.hass.panels[panel];
|
||||||
icon: this.hass.panels.light.icon || "mdi:lamps",
|
if (!panel) {
|
||||||
title: this.hass.localize("panel.light"),
|
return;
|
||||||
|
}
|
||||||
|
const item: DataTableItem = {
|
||||||
|
icon: getPanelIcon(panelInfo),
|
||||||
|
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
|
||||||
show_in_sidebar: true,
|
show_in_sidebar: true,
|
||||||
mode: "storage",
|
mode: "storage",
|
||||||
url_path: "light",
|
url_path: panelInfo.url_path,
|
||||||
filename: "",
|
filename: "",
|
||||||
default: false,
|
default: defaultUrlPath === panelInfo.url_path,
|
||||||
require_admin: false,
|
require_admin: false,
|
||||||
type: this._localizeType("built_in"),
|
type: "built_in",
|
||||||
});
|
localized_type: this._localizeType("built_in"),
|
||||||
}
|
};
|
||||||
|
result.push(item);
|
||||||
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"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push(
|
result.push(
|
||||||
...dashboards
|
...dashboards
|
||||||
@@ -386,7 +355,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
filename: "",
|
filename: "",
|
||||||
...dashboard,
|
...dashboard,
|
||||||
default: defaultUrlPath === dashboard.url_path,
|
default: defaultUrlPath === dashboard.url_path,
|
||||||
type: this._localizeType("user_created"),
|
type: "user_created",
|
||||||
|
localized_type: this._localizeType("user_created"),
|
||||||
}) satisfies DataTableItem
|
}) satisfies DataTableItem
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -486,20 +456,32 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
this._openDetailDialog(dashboard, urlPath);
|
this._openDetailDialog(dashboard, urlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _canDelete(urlPath: string) {
|
private _handleSetAsDefault = async (item: DataTableItem) => {
|
||||||
return ![
|
if (item.default) {
|
||||||
"lovelace",
|
return;
|
||||||
"energy",
|
}
|
||||||
"light",
|
|
||||||
"security",
|
|
||||||
"climate",
|
|
||||||
"home",
|
|
||||||
].includes(urlPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _canEdit(urlPath: string) {
|
const confirm = await showConfirmationDialog(this, {
|
||||||
return !["light", "security", "climate", "home"].includes(urlPath);
|
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) => {
|
private _handleDelete = async (item: DataTableItem) => {
|
||||||
const dashboard = this._dashboards.find(
|
const dashboard = this._dashboards.find(
|
||||||
@@ -581,10 +563,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
|||||||
private async _deleteDashboard(
|
private async _deleteDashboard(
|
||||||
dashboard: LovelaceDashboard
|
dashboard: LovelaceDashboard
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
if (!this._canDelete(dashboard.url_path)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirm = await showConfirmationDialog(this, {
|
const confirm = await showConfirmationDialog(this, {
|
||||||
title: this.hass!.localize(
|
title: this.hass!.localize(
|
||||||
"ui.panel.config.lovelace.dashboards.confirm_delete_title",
|
"ui.panel.config.lovelace.dashboards.confirm_delete_title",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
import { LitElement, css, html } from "lit";
|
import { LitElement, css, html } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { repeat } from "lit/directives/repeat";
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
|
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
|
||||||
import type {
|
import type {
|
||||||
PipelineRunEvent,
|
PipelineRunEvent,
|
||||||
@@ -20,6 +21,8 @@ import "../../../../layouts/hass-subpage";
|
|||||||
import { haStyle } from "../../../../resources/styles";
|
import { haStyle } from "../../../../resources/styles";
|
||||||
import type { HomeAssistant, Route } from "../../../../types";
|
import type { HomeAssistant, Route } from "../../../../types";
|
||||||
import "./assist-render-pipeline-events";
|
import "./assist-render-pipeline-events";
|
||||||
|
import type { ChatLog } from "../../../../data/chat_log";
|
||||||
|
import { subscribeChatLog } from "../../../../data/chat_log";
|
||||||
|
|
||||||
@customElement("assist-pipeline-debug")
|
@customElement("assist-pipeline-debug")
|
||||||
export class AssistPipelineDebug extends LitElement {
|
export class AssistPipelineDebug extends LitElement {
|
||||||
@@ -37,8 +40,12 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
|
|
||||||
@state() private _events?: PipelineRunEvent[];
|
@state() private _events?: PipelineRunEvent[];
|
||||||
|
|
||||||
|
@state() private _chatLog?: ChatLog;
|
||||||
|
|
||||||
private _unsubRefreshEventsID?: number;
|
private _unsubRefreshEventsID?: number;
|
||||||
|
|
||||||
|
private _unsubChatLogUpdates?: Promise<UnsubscribeFunc>;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`<hass-subpage
|
return html`<hass-subpage
|
||||||
.narrow=${this.narrow}
|
.narrow=${this.narrow}
|
||||||
@@ -106,6 +113,7 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
? html`<assist-render-pipeline-events
|
? html`<assist-render-pipeline-events
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.events=${this._events}
|
.events=${this._events}
|
||||||
|
.chatLog=${this._chatLog}
|
||||||
></assist-render-pipeline-events>`
|
></assist-render-pipeline-events>`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
@@ -120,6 +128,10 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
clearRefresh = true;
|
clearRefresh = true;
|
||||||
}
|
}
|
||||||
if (changedProperties.has("_runId")) {
|
if (changedProperties.has("_runId")) {
|
||||||
|
if (this._unsubChatLogUpdates) {
|
||||||
|
this._unsubChatLogUpdates.then((unsub) => unsub());
|
||||||
|
this._unsubChatLogUpdates = undefined;
|
||||||
|
}
|
||||||
this._fetchEvents();
|
this._fetchEvents();
|
||||||
clearRefresh = true;
|
clearRefresh = true;
|
||||||
}
|
}
|
||||||
@@ -135,6 +147,10 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
clearTimeout(this._unsubRefreshEventsID);
|
clearTimeout(this._unsubRefreshEventsID);
|
||||||
this._unsubRefreshEventsID = undefined;
|
this._unsubRefreshEventsID = undefined;
|
||||||
}
|
}
|
||||||
|
if (this._unsubChatLogUpdates) {
|
||||||
|
this._unsubChatLogUpdates.then((unsub) => unsub());
|
||||||
|
this._unsubChatLogUpdates = undefined;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetchRuns() {
|
private async _fetchRuns() {
|
||||||
@@ -185,8 +201,27 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!this._events!.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!this._unsubChatLogUpdates && this._events[0].type === "run-start") {
|
||||||
|
this._unsubChatLogUpdates = subscribeChatLog(
|
||||||
|
this.hass,
|
||||||
|
this._events[0].data.conversation_id,
|
||||||
|
(chatLog) => {
|
||||||
|
if (chatLog) {
|
||||||
|
this._chatLog = chatLog;
|
||||||
|
} else {
|
||||||
|
this._unsubChatLogUpdates?.then((unsub) => unsub());
|
||||||
|
this._unsubChatLogUpdates = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this._unsubChatLogUpdates.catch(() => {
|
||||||
|
this._unsubChatLogUpdates = undefined;
|
||||||
|
});
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this._events?.length &&
|
|
||||||
// If the last event is not a finish run event, the run is still ongoing.
|
// If the last event is not a finish run event, the run is still ongoing.
|
||||||
// Refresh events automatically.
|
// Refresh events automatically.
|
||||||
!["run-end", "error"].includes(this._events[this._events.length - 1].type)
|
!["run-end", "error"].includes(this._events[this._events.length - 1].type)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { TemplateResult } from "lit";
|
import type { TemplateResult } from "lit";
|
||||||
import { css, html, LitElement } from "lit";
|
import { css, html, LitElement } from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import { extractSearchParam } from "../../../../common/url/search-params";
|
import { extractSearchParam } from "../../../../common/url/search-params";
|
||||||
import "../../../../components/ha-assist-pipeline-picker";
|
import "../../../../components/ha-assist-pipeline-picker";
|
||||||
import "../../../../components/ha-button";
|
import "../../../../components/ha-button";
|
||||||
@@ -24,6 +25,8 @@ import type { HomeAssistant } from "../../../../types";
|
|||||||
import { AudioRecorder } from "../../../../util/audio-recorder";
|
import { AudioRecorder } from "../../../../util/audio-recorder";
|
||||||
import { fileDownload } from "../../../../util/file_download";
|
import { fileDownload } from "../../../../util/file_download";
|
||||||
import "./assist-render-pipeline-run";
|
import "./assist-render-pipeline-run";
|
||||||
|
import type { ChatLog } from "../../../../data/chat_log";
|
||||||
|
import { subscribeChatLog } from "../../../../data/chat_log";
|
||||||
|
|
||||||
@customElement("assist-pipeline-run-debug")
|
@customElement("assist-pipeline-run-debug")
|
||||||
export class AssistPipelineRunDebug extends LitElement {
|
export class AssistPipelineRunDebug extends LitElement {
|
||||||
@@ -46,6 +49,13 @@ export class AssistPipelineRunDebug extends LitElement {
|
|||||||
@state() private _pipelineId?: string =
|
@state() private _pipelineId?: string =
|
||||||
extractSearchParam("pipeline") || undefined;
|
extractSearchParam("pipeline") || undefined;
|
||||||
|
|
||||||
|
@state() private _chatLog?: ChatLog;
|
||||||
|
|
||||||
|
private _chatLogSubscription: {
|
||||||
|
conversationId: string;
|
||||||
|
unsub: Promise<UnsubscribeFunc>;
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<hass-subpage
|
<hass-subpage
|
||||||
@@ -178,6 +188,7 @@ export class AssistPipelineRunDebug extends LitElement {
|
|||||||
<assist-render-pipeline-run
|
<assist-render-pipeline-run
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.pipelineRun=${run}
|
.pipelineRun=${run}
|
||||||
|
.chatLog=${this._chatLog}
|
||||||
></assist-render-pipeline-run>
|
></assist-render-pipeline-run>
|
||||||
`
|
`
|
||||||
)}
|
)}
|
||||||
@@ -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 {
|
private get conversationId(): string | null {
|
||||||
return this._pipelineRuns.length === 0
|
return this._pipelineRuns.length === 0
|
||||||
? null
|
? null
|
||||||
@@ -408,6 +427,32 @@ export class AssistPipelineRunDebug extends LitElement {
|
|||||||
added = true;
|
added = true;
|
||||||
}
|
}
|
||||||
callback(updatedRun);
|
callback(updatedRun);
|
||||||
|
|
||||||
|
const conversationId = this.conversationId;
|
||||||
|
if (
|
||||||
|
!this._chatLog &&
|
||||||
|
conversationId &&
|
||||||
|
(!this._chatLogSubscription ||
|
||||||
|
this._chatLogSubscription.conversationId !== conversationId)
|
||||||
|
) {
|
||||||
|
if (this._chatLogSubscription) {
|
||||||
|
this._chatLogSubscription.unsub.then((unsub) => unsub());
|
||||||
|
}
|
||||||
|
this._chatLogSubscription = {
|
||||||
|
conversationId,
|
||||||
|
unsub: subscribeChatLog(this.hass, conversationId, (chatLog) => {
|
||||||
|
if (chatLog) {
|
||||||
|
this._chatLog = chatLog;
|
||||||
|
} else {
|
||||||
|
this._chatLogSubscription?.unsub.then((unsub) => unsub());
|
||||||
|
this._chatLogSubscription = null;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
this._chatLogSubscription.unsub.catch(() => {
|
||||||
|
this._chatLogSubscription = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
...options,
|
...options,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
import { processEvent } from "../../../../data/assist_pipeline";
|
import { processEvent } from "../../../../data/assist_pipeline";
|
||||||
import type { HomeAssistant } from "../../../../types";
|
import type { HomeAssistant } from "../../../../types";
|
||||||
import "./assist-render-pipeline-run";
|
import "./assist-render-pipeline-run";
|
||||||
|
import type { ChatLog } from "../../../../data/chat_log";
|
||||||
|
|
||||||
@customElement("assist-render-pipeline-events")
|
@customElement("assist-render-pipeline-events")
|
||||||
export class AssistPipelineEvents extends LitElement {
|
export class AssistPipelineEvents extends LitElement {
|
||||||
@@ -16,6 +17,8 @@ export class AssistPipelineEvents extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public events!: PipelineRunEvent[];
|
@property({ attribute: false }) public events!: PipelineRunEvent[];
|
||||||
|
|
||||||
|
@property({ attribute: false }) public chatLog?: ChatLog;
|
||||||
|
|
||||||
private _processEvents = memoizeOne(
|
private _processEvents = memoizeOne(
|
||||||
(events: PipelineRunEvent[]): PipelineRun | undefined => {
|
(events: PipelineRunEvent[]): PipelineRun | undefined => {
|
||||||
let run: PipelineRun | undefined;
|
let run: PipelineRun | undefined;
|
||||||
@@ -56,6 +59,7 @@ export class AssistPipelineEvents extends LitElement {
|
|||||||
<assist-render-pipeline-run
|
<assist-render-pipeline-run
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.pipelineRun=${run}
|
.pipelineRun=${run}
|
||||||
|
.chatLog=${this.chatLog}
|
||||||
></assist-render-pipeline-run>
|
></assist-render-pipeline-run>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { TemplateResult } from "lit";
|
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 { customElement, property } from "lit/decorators";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
@@ -12,6 +12,12 @@ import { formatNumber } from "../../../../common/number/format_number";
|
|||||||
import "../../../../components/ha-yaml-editor";
|
import "../../../../components/ha-yaml-editor";
|
||||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||||
|
import type {
|
||||||
|
ChatLogAssistantContent,
|
||||||
|
ChatLog,
|
||||||
|
ChatLogContent,
|
||||||
|
ChatLogUserContent,
|
||||||
|
} from "../../../../data/chat_log";
|
||||||
|
|
||||||
const RUN_DATA = ["pipeline", "language"];
|
const RUN_DATA = ["pipeline", "language"];
|
||||||
const WAKE_WORD_DATA = ["engine"];
|
const WAKE_WORD_DATA = ["engine"];
|
||||||
@@ -119,7 +125,7 @@ const dataMinusKeysRender = (
|
|||||||
result[key] = data[key];
|
result[key] = data[key];
|
||||||
}
|
}
|
||||||
return render
|
return render
|
||||||
? html`<ha-expansion-panel>
|
? html`<ha-expansion-panel class="yaml-expansion">
|
||||||
<span slot="header"
|
<span slot="header"
|
||||||
>${hass.localize("ui.panel.config.voice_assistants.debug.raw")}</span
|
>${hass.localize("ui.panel.config.voice_assistants.debug.raw")}</span
|
||||||
>
|
>
|
||||||
@@ -134,6 +140,8 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public pipelineRun!: PipelineRun;
|
@property({ attribute: false }) public pipelineRun!: PipelineRun;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public chatLog?: ChatLog;
|
||||||
|
|
||||||
private _audioElement?: HTMLAudioElement;
|
private _audioElement?: HTMLAudioElement;
|
||||||
|
|
||||||
private get _isPlaying(): boolean {
|
private get _isPlaying(): boolean {
|
||||||
@@ -147,31 +155,47 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
) || "ready"
|
) || "ready"
|
||||||
: "ready";
|
: "ready";
|
||||||
|
|
||||||
const messages: { from: string; text: string }[] = [];
|
let messages: ChatLogContent[];
|
||||||
|
|
||||||
const userMessage =
|
if (this.chatLog) {
|
||||||
(this.pipelineRun.init_options &&
|
messages = this.chatLog.content.filter(
|
||||||
"text" in this.pipelineRun.init_options.input
|
this.pipelineRun.finished
|
||||||
? this.pipelineRun.init_options.input.text
|
? (content: ChatLogContent) =>
|
||||||
: undefined) ||
|
content.role === "system" ||
|
||||||
this.pipelineRun?.stt?.stt_output?.text ||
|
(content.created >= this.pipelineRun.started &&
|
||||||
this.pipelineRun?.intent?.intent_input;
|
content.created <= this.pipelineRun.finished!)
|
||||||
|
: (content: ChatLogContent) =>
|
||||||
|
content.role === "system" ||
|
||||||
|
content.created >= this.pipelineRun.started
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
messages = [];
|
||||||
|
|
||||||
if (userMessage) {
|
// We don't have the chat log everywhere yet, just fallback for now.
|
||||||
messages.push({
|
const userMessage =
|
||||||
from: "user",
|
(this.pipelineRun.init_options &&
|
||||||
text: userMessage,
|
"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 (
|
if (userMessage) {
|
||||||
this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
|
messages.push({
|
||||||
) {
|
role: "user",
|
||||||
messages.push({
|
content: userMessage,
|
||||||
from: "hass",
|
} as ChatLogUserContent);
|
||||||
text: this.pipelineRun.intent.intent_output.response.speech.plain
|
}
|
||||||
.speech,
|
|
||||||
});
|
if (
|
||||||
|
this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
|
||||||
|
) {
|
||||||
|
messages.push({
|
||||||
|
role: "assistant",
|
||||||
|
content:
|
||||||
|
this.pipelineRun.intent.intent_output.response.speech.plain.speech,
|
||||||
|
} as ChatLogAssistantContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
@@ -190,10 +214,58 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
${messages.length > 0
|
${messages.length > 0
|
||||||
? html`
|
? html`
|
||||||
<div class="messages">
|
<div class="messages">
|
||||||
${messages.map(
|
${messages.map((content) =>
|
||||||
({ from, text }) => html`
|
content.role === "system" || content.role === "tool_result"
|
||||||
<div class=${`message ${from}`}>${text}</div>
|
? 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>
|
||||||
<div style="clear:both"></div>
|
<div style="clear:both"></div>
|
||||||
@@ -442,7 +514,7 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
|
${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<ha-expansion-panel>
|
<ha-expansion-panel class="yaml-expansion">
|
||||||
<span slot="header"
|
<span slot="header"
|
||||||
>${this.hass.localize(
|
>${this.hass.localize(
|
||||||
"ui.panel.config.voice_assistants.debug.raw"
|
"ui.panel.config.voice_assistants.debug.raw"
|
||||||
@@ -519,12 +591,12 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
.row > div:last-child {
|
.row > div:last-child {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
ha-expansion-panel {
|
.yaml-expansion {
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
padding-inline-start: 8px;
|
padding-inline-start: 8px;
|
||||||
padding-inline-end: initial;
|
padding-inline-end: initial;
|
||||||
}
|
}
|
||||||
.card-content ha-expansion-panel {
|
.card-content .yaml-expansion {
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
padding-inline-start: 0px;
|
padding-inline-start: 0px;
|
||||||
padding-inline-end: initial;
|
padding-inline-end: initial;
|
||||||
@@ -540,27 +612,59 @@ export class AssistPipelineDebug extends LitElement {
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-expansion {
|
||||||
|
margin: 8px 0;
|
||||||
|
border-radius: var(--ha-border-radius-xl);
|
||||||
|
clear: both;
|
||||||
|
padding: 0 8px;
|
||||||
|
--input-fill-color: none;
|
||||||
|
max-width: calc(100% - 24px);
|
||||||
|
--expansion-panel-summary-padding: 0px;
|
||||||
|
--expansion-panel-content-padding: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-expansion *[slot="header"] {
|
||||||
|
font-weight: var(--ha-font-weight-normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
.system {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message,
|
||||||
|
.content-expansion {
|
||||||
font-size: var(--ha-font-size-l);
|
font-size: var(--ha-font-size-l);
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
padding: 8px;
|
|
||||||
border-radius: var(--ha-border-radius-xl);
|
border-radius: var(--ha-border-radius-xl);
|
||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.user {
|
.messages pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user,
|
||||||
|
.tool_result {
|
||||||
margin-left: 24px;
|
margin-left: 24px;
|
||||||
margin-inline-start: 24px;
|
margin-inline-start: 24px;
|
||||||
margin-inline-end: initial;
|
margin-inline-end: initial;
|
||||||
float: var(--float-end);
|
float: var(--float-end);
|
||||||
text-align: right;
|
|
||||||
border-bottom-right-radius: 0px;
|
border-bottom-right-radius: 0px;
|
||||||
background-color: var(--light-primary-color);
|
background-color: var(--light-primary-color);
|
||||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||||
direction: var(--direction);
|
direction: var(--direction);
|
||||||
}
|
}
|
||||||
|
|
||||||
.message.hass {
|
.message.user,
|
||||||
|
.content-expansion div[slot="header"] {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assistant {
|
||||||
margin-right: 24px;
|
margin-right: 24px;
|
||||||
margin-inline-end: 24px;
|
margin-inline-end: 24px;
|
||||||
margin-inline-start: initial;
|
margin-inline-start: initial;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { formatTimeWithSeconds } from "../../common/datetime/format_time";
|
|||||||
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
import { restoreScroll } from "../../common/decorators/restore-scroll";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import { computeDomain } from "../../common/entity/compute_domain";
|
import { computeDomain } from "../../common/entity/compute_domain";
|
||||||
|
import { DEFAULT_ENTITY_NAME } from "../../common/entity/compute_entity_name_display";
|
||||||
import { navigate } from "../../common/navigate";
|
import { navigate } from "../../common/navigate";
|
||||||
import { computeTimelineColor } from "../../components/chart/timeline-color";
|
import { computeTimelineColor } from "../../components/chart/timeline-color";
|
||||||
import "../../components/entity/state-badge";
|
import "../../components/entity/state-badge";
|
||||||
@@ -463,15 +464,24 @@ class HaLogbookRenderer extends LitElement {
|
|||||||
entityName: string | undefined,
|
entityName: string | undefined,
|
||||||
noLink?: boolean
|
noLink?: boolean
|
||||||
) {
|
) {
|
||||||
const hasState = entityId && entityId in this.hass.states;
|
if (!entityId) {
|
||||||
const displayName =
|
return entityName || "";
|
||||||
entityName ||
|
}
|
||||||
(hasState
|
|
||||||
? this.hass.states[entityId].attributes.friendly_name || entityId
|
const stateObj = this.hass.states[entityId];
|
||||||
: 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) {
|
if (!hasState) {
|
||||||
return displayName;
|
return displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
return noLink
|
return noLink
|
||||||
? displayName
|
? displayName
|
||||||
: html`<button
|
: html`<button
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { getEnergyColor } from "./common/color";
|
|||||||
import { formatNumber } from "../../../../common/number/format_number";
|
import { formatNumber } from "../../../../common/number/format_number";
|
||||||
import "../../../../components/chart/ha-chart-base";
|
import "../../../../components/chart/ha-chart-base";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
|
import "./common/hui-energy-graph-chip";
|
||||||
import type {
|
import type {
|
||||||
EnergyData,
|
EnergyData,
|
||||||
EnergySumData,
|
EnergySumData,
|
||||||
@@ -67,6 +68,8 @@ export class HuiEnergyUsageGraphCard
|
|||||||
|
|
||||||
@state() private _compareEnd?: Date;
|
@state() private _compareEnd?: Date;
|
||||||
|
|
||||||
|
@state() private _total?: number;
|
||||||
|
|
||||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||||
|
|
||||||
public hassSubscribe(): UnsubscribeFunc[] {
|
public hassSubscribe(): UnsubscribeFunc[] {
|
||||||
@@ -100,9 +103,19 @@ export class HuiEnergyUsageGraphCard
|
|||||||
|
|
||||||
return html`
|
return html`
|
||||||
<ha-card>
|
<ha-card>
|
||||||
${this._config.title
|
<div class="card-header">
|
||||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
<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
|
<div
|
||||||
class="content ${classMap({
|
class="content ${classMap({
|
||||||
"has-header": !!this._config.title,
|
"has-header": !!this._config.title,
|
||||||
@@ -338,6 +351,13 @@ export class HuiEnergyUsageGraphCard
|
|||||||
datasets.sort((a, b) => a.order - b.order);
|
datasets.sort((a, b) => a.order - b.order);
|
||||||
fillDataGapsAndRoundCaps(datasets);
|
fillDataGapsAndRoundCaps(datasets);
|
||||||
this._chartData = 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(
|
private _processDataSet(
|
||||||
@@ -515,6 +535,9 @@ export class HuiEnergyUsageGraphCard
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
.card-header {
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
}
|
}
|
||||||
.content {
|
.content {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ReactiveElement } from "lit";
|
import { ReactiveElement } from "lit";
|
||||||
import { customElement } from "lit/decorators";
|
import { customElement } from "lit/decorators";
|
||||||
|
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||||
import {
|
import {
|
||||||
findEntities,
|
findEntities,
|
||||||
@@ -23,8 +24,8 @@ import type {
|
|||||||
WeatherForecastCardConfig,
|
WeatherForecastCardConfig,
|
||||||
} from "../../cards/types";
|
} from "../../cards/types";
|
||||||
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
import type { CommonControlSectionStrategyConfig } from "../usage_prediction/common-controls-section-strategy";
|
||||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
|
||||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||||
|
import type { Condition } from "../../common/validate-condition";
|
||||||
|
|
||||||
export interface HomeMainViewStrategyConfig {
|
export interface HomeMainViewStrategyConfig {
|
||||||
type: "home-main";
|
type: "home-main";
|
||||||
@@ -70,8 +71,12 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
|
|
||||||
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
const floorCount = home.floors.length + (home.areas.length ? 1 : 0);
|
||||||
|
|
||||||
// Allow between 2 and 3 columns (the max should be set to define the width of the header)
|
const maxColumns = 3;
|
||||||
const maxColumns = 2;
|
|
||||||
|
const largeScreenCondition: Condition = {
|
||||||
|
condition: "screen",
|
||||||
|
media_query: "(min-width: 871px)",
|
||||||
|
};
|
||||||
|
|
||||||
const floorsSections: LovelaceSectionConfig[] = [];
|
const floorsSections: LovelaceSectionConfig[] = [];
|
||||||
for (const floorStructure of home.floors) {
|
for (const floorStructure of home.floors) {
|
||||||
@@ -126,12 +131,6 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const favoriteSection: LovelaceSectionConfig = {
|
|
||||||
type: "grid",
|
|
||||||
column_span: maxColumns,
|
|
||||||
cards: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const favoriteEntities = (config.favorite_entities || []).filter(
|
const favoriteEntities = (config.favorite_entities || []).filter(
|
||||||
(entityId) => hass.states[entityId] !== undefined
|
(entityId) => hass.states[entityId] !== undefined
|
||||||
);
|
);
|
||||||
@@ -176,74 +175,70 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
({
|
({
|
||||||
type: "home-summary",
|
type: "home-summary",
|
||||||
summary: "light",
|
summary: "light",
|
||||||
vertical: true,
|
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "/light?historyBack=1",
|
navigation_path: "/light?historyBack=1",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
columns: 12,
|
||||||
columns: 4,
|
|
||||||
},
|
},
|
||||||
} satisfies HomeSummaryCard),
|
} satisfies HomeSummaryCard),
|
||||||
hasClimate &&
|
hasClimate &&
|
||||||
({
|
({
|
||||||
type: "home-summary",
|
type: "home-summary",
|
||||||
summary: "climate",
|
summary: "climate",
|
||||||
vertical: true,
|
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "/climate?historyBack=1",
|
navigation_path: "/climate?historyBack=1",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
columns: 12,
|
||||||
columns: 4,
|
|
||||||
},
|
},
|
||||||
} satisfies HomeSummaryCard),
|
} satisfies HomeSummaryCard),
|
||||||
hasSecurity &&
|
hasSecurity &&
|
||||||
({
|
({
|
||||||
type: "home-summary",
|
type: "home-summary",
|
||||||
summary: "security",
|
summary: "security",
|
||||||
vertical: true,
|
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "/security?historyBack=1",
|
navigation_path: "/security?historyBack=1",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
columns: 12,
|
||||||
columns: 4,
|
|
||||||
},
|
},
|
||||||
} satisfies HomeSummaryCard),
|
} satisfies HomeSummaryCard),
|
||||||
hasMediaPlayers &&
|
hasMediaPlayers &&
|
||||||
({
|
({
|
||||||
type: "home-summary",
|
type: "home-summary",
|
||||||
summary: "media_players",
|
summary: "media_players",
|
||||||
vertical: true,
|
|
||||||
tap_action: {
|
tap_action: {
|
||||||
action: "navigate",
|
action: "navigate",
|
||||||
navigation_path: "media-players",
|
navigation_path: "media-players",
|
||||||
},
|
},
|
||||||
grid_options: {
|
grid_options: {
|
||||||
rows: 2,
|
columns: 12,
|
||||||
columns: 4,
|
|
||||||
},
|
},
|
||||||
} satisfies HomeSummaryCard),
|
} satisfies HomeSummaryCard),
|
||||||
].filter(Boolean) as LovelaceCardConfig[];
|
].filter(Boolean) as LovelaceCardConfig[];
|
||||||
|
|
||||||
const summarySection: LovelaceSectionConfig = {
|
const forYouSection: LovelaceSectionConfig = {
|
||||||
type: "grid",
|
type: "grid",
|
||||||
column_span: maxColumns,
|
cards: [
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
heading: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
|
||||||
|
heading_style: "title",
|
||||||
|
visibility: [largeScreenCondition],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const widgetSection: LovelaceSectionConfig = {
|
||||||
cards: [],
|
cards: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
if (summaryCards.length) {
|
if (summaryCards.length) {
|
||||||
summarySection.cards!.push(
|
widgetSection.cards!.push(...summaryCards);
|
||||||
{
|
|
||||||
type: "heading",
|
|
||||||
heading: hass.localize("ui.panel.lovelace.strategy.home.summaries"),
|
|
||||||
},
|
|
||||||
...summaryCards
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const weatherFilter = generateEntityFilter(hass, {
|
const weatherFilter = generateEntityFilter(hass, {
|
||||||
@@ -251,28 +246,16 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
entity_category: "none",
|
entity_category: "none",
|
||||||
});
|
});
|
||||||
|
|
||||||
const widgetSection: LovelaceSectionConfig = {
|
|
||||||
type: "grid",
|
|
||||||
column_span: maxColumns,
|
|
||||||
cards: [],
|
|
||||||
};
|
|
||||||
const weatherEntity = Object.keys(hass.states)
|
const weatherEntity = Object.keys(hass.states)
|
||||||
.filter(weatherFilter)
|
.filter(weatherFilter)
|
||||||
.sort()[0];
|
.sort()[0];
|
||||||
|
|
||||||
if (weatherEntity) {
|
if (weatherEntity) {
|
||||||
widgetSection.cards!.push(
|
widgetSection.cards!.push({
|
||||||
{
|
type: "weather-forecast",
|
||||||
type: "heading",
|
entity: weatherEntity,
|
||||||
heading: "",
|
forecast_type: "daily",
|
||||||
heading_style: "subtitle",
|
} as WeatherForecastCardConfig);
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "weather-forecast",
|
|
||||||
entity: weatherEntity,
|
|
||||||
forecast_type: "daily",
|
|
||||||
} as WeatherForecastCardConfig
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const energyPrefs = isComponentLoaded(hass, "energy")
|
const energyPrefs = isComponentLoaded(hass, "energy")
|
||||||
@@ -299,11 +282,19 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
|
|
||||||
const sections = (
|
const sections = (
|
||||||
[
|
[
|
||||||
favoriteSection.cards && favoriteSection,
|
{
|
||||||
|
type: "grid",
|
||||||
|
cards: [
|
||||||
|
// Heading to add some spacing on large screens
|
||||||
|
{
|
||||||
|
type: "heading",
|
||||||
|
heading_style: "subtitle",
|
||||||
|
visibility: [largeScreenCondition],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
commonControlsSection,
|
commonControlsSection,
|
||||||
summarySection.cards && summarySection,
|
|
||||||
...floorsSections,
|
...floorsSections,
|
||||||
widgetSection.cards && widgetSection,
|
|
||||||
] satisfies (LovelaceSectionRawConfig | undefined)[]
|
] satisfies (LovelaceSectionRawConfig | undefined)[]
|
||||||
).filter(Boolean) as LovelaceSectionRawConfig[];
|
).filter(Boolean) as LovelaceSectionRawConfig[];
|
||||||
|
|
||||||
@@ -319,6 +310,11 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
|||||||
content: `## ${hass.localize("ui.panel.lovelace.strategy.home.welcome_user", { user: "{{ user }}" })}`,
|
content: `## ${hass.localize("ui.panel.lovelace.strategy.home.welcome_user", { user: "{{ user }}" })}`,
|
||||||
} satisfies MarkdownCardConfig,
|
} satisfies MarkdownCardConfig,
|
||||||
},
|
},
|
||||||
|
sidebar: {
|
||||||
|
sections: [forYouSection, widgetSection],
|
||||||
|
content_label: hass.localize("ui.panel.lovelace.strategy.home.home"),
|
||||||
|
sidebar_label: hass.localize("ui.panel.lovelace.strategy.home.for_you"),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import {
|
|||||||
import type { HuiSection } from "../sections/hui-section";
|
import type { HuiSection } from "../sections/hui-section";
|
||||||
import type { Lovelace } from "../types";
|
import type { Lovelace } from "../types";
|
||||||
import "./hui-view-header";
|
import "./hui-view-header";
|
||||||
|
import "./hui-view-sidebar";
|
||||||
|
|
||||||
export const DEFAULT_MAX_COLUMNS = 4;
|
export const DEFAULT_MAX_COLUMNS = 4;
|
||||||
|
|
||||||
@@ -46,6 +47,8 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
@property({ attribute: false }) public isStrategy = false;
|
@property({ attribute: false }) public isStrategy = false;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow = false;
|
||||||
|
|
||||||
@property({ attribute: false }) public sections: HuiSection[] = [];
|
@property({ attribute: false }) public sections: HuiSection[] = [];
|
||||||
|
|
||||||
@property({ attribute: false }) public cards: HuiCard[] = [];
|
@property({ attribute: false }) public cards: HuiCard[] = [];
|
||||||
@@ -58,6 +61,12 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
@state() _dragging = false;
|
@state() _dragging = false;
|
||||||
|
|
||||||
|
@state() private _showSidebar = false;
|
||||||
|
|
||||||
|
private _contentScrollTop = 0;
|
||||||
|
|
||||||
|
private _sidebarScrollTop = 0;
|
||||||
|
|
||||||
private _columnsController = new ResizeController(this, {
|
private _columnsController = new ResizeController(this, {
|
||||||
callback: (entries) => {
|
callback: (entries) => {
|
||||||
const totalWidth = entries[0]?.contentRect.width;
|
const totalWidth = entries[0]?.contentRect.width;
|
||||||
@@ -135,16 +144,31 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
const sections = this.sections;
|
const sections = this.sections;
|
||||||
const totalSectionCount =
|
const totalSectionCount =
|
||||||
this._sectionColumnCount + (this.lovelace?.editMode ? 1 : 0);
|
this._sectionColumnCount +
|
||||||
|
(this.lovelace?.editMode ? 1 : 0) +
|
||||||
|
(this._config?.sidebar ? 1 : 0);
|
||||||
const editMode = this.lovelace.editMode;
|
const editMode = this.lovelace.editMode;
|
||||||
|
|
||||||
const maxColumnCount = this._columnsController.value ?? 1;
|
const maxColumnCount = this._columnsController.value ?? 1;
|
||||||
|
|
||||||
|
const columnCount = Math.min(maxColumnCount, totalSectionCount);
|
||||||
|
// On mobile with sidebar, use full width for whichever view is active
|
||||||
|
const contentColumnCount =
|
||||||
|
this._config?.sidebar && !this.narrow
|
||||||
|
? Math.max(1, columnCount - 1)
|
||||||
|
: columnCount;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="wrapper ${classMap({
|
class="wrapper ${classMap({
|
||||||
"top-margin": Boolean(this._config?.top_margin),
|
"top-margin": Boolean(this._config?.top_margin),
|
||||||
|
"has-sidebar": Boolean(this._config?.sidebar),
|
||||||
|
narrow: this.narrow,
|
||||||
})}"
|
})}"
|
||||||
|
style=${styleMap({
|
||||||
|
"--column-count": columnCount,
|
||||||
|
"--content-column-count": contentColumnCount,
|
||||||
|
})}
|
||||||
>
|
>
|
||||||
<hui-view-header
|
<hui-view-header
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
@@ -152,38 +176,54 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
.lovelace=${this.lovelace}
|
.lovelace=${this.lovelace}
|
||||||
.viewIndex=${this.index}
|
.viewIndex=${this.index}
|
||||||
.config=${this._config?.header}
|
.config=${this._config?.header}
|
||||||
style=${styleMap({
|
|
||||||
"--max-column-count": maxColumnCount,
|
|
||||||
})}
|
|
||||||
></hui-view-header>
|
></hui-view-header>
|
||||||
<ha-sortable
|
${this.narrow && this._config?.sidebar
|
||||||
.disabled=${!editMode}
|
? html`
|
||||||
@item-moved=${this._sectionMoved}
|
<div class="mobile-tabs">
|
||||||
group="section"
|
<ha-control-select
|
||||||
handle-selector=".handle"
|
.value=${this._showSidebar ? "sidebar" : "content"}
|
||||||
draggable-selector=".section"
|
@value-changed=${this._viewChanged}
|
||||||
.rollback=${false}
|
.options=${[
|
||||||
>
|
{
|
||||||
<div
|
value: "content",
|
||||||
class="container ${classMap({
|
label: this._config.sidebar.content_label,
|
||||||
dense: Boolean(this._config?.dense_section_placement),
|
},
|
||||||
})}"
|
{
|
||||||
style=${styleMap({
|
value: "sidebar",
|
||||||
"--total-section-count": totalSectionCount,
|
label: this._config.sidebar.sidebar_label,
|
||||||
"--max-column-count": maxColumnCount,
|
},
|
||||||
})}
|
]}
|
||||||
|
>
|
||||||
|
</ha-control-select>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
<div class="container">
|
||||||
|
<ha-sortable
|
||||||
|
.disabled=${!editMode}
|
||||||
|
@item-moved=${this._sectionMoved}
|
||||||
|
group="section"
|
||||||
|
handle-selector=".handle"
|
||||||
|
draggable-selector=".section"
|
||||||
|
.rollback=${false}
|
||||||
>
|
>
|
||||||
${repeat(
|
<div
|
||||||
sections,
|
class="content ${classMap({
|
||||||
(section) => this._getSectionKey(section),
|
dense: Boolean(this._config?.dense_section_placement),
|
||||||
(section, idx) => {
|
"mobile-hidden": this.narrow && this._showSidebar,
|
||||||
const columnSpan = Math.min(
|
})}"
|
||||||
section.config.column_span || 1,
|
>
|
||||||
maxColumnCount
|
${repeat(
|
||||||
);
|
sections,
|
||||||
const rowSpan = section.config.row_span || 1;
|
(section) => this._getSectionKey(section),
|
||||||
|
(section, idx) => {
|
||||||
|
const columnSpan = Math.min(
|
||||||
|
section.config.column_span || 1,
|
||||||
|
contentColumnCount
|
||||||
|
);
|
||||||
|
const rowSpan = section.config.row_span || 1;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
class="section"
|
class="section"
|
||||||
style=${styleMap({
|
style=${styleMap({
|
||||||
@@ -208,72 +248,89 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
${editMode
|
${editMode
|
||||||
? html`
|
? html`
|
||||||
<ha-sortable
|
<ha-sortable
|
||||||
group="card"
|
group="card"
|
||||||
@item-added=${this._handleCardAdded}
|
@item-added=${this._handleCardAdded}
|
||||||
draggable-selector=".card"
|
draggable-selector=".card"
|
||||||
.rollback=${false}
|
.rollback=${false}
|
||||||
>
|
>
|
||||||
<div class="create-section-container">
|
<div class="create-section-container">
|
||||||
<div class="drop-helper" aria-hidden="true">
|
<div class="drop-helper" aria-hidden="true">
|
||||||
<p>
|
<p>
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.panel.lovelace.editor.section.drop_card_create_section"
|
"ui.panel.lovelace.editor.section.drop_card_create_section"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="create-section"
|
||||||
|
@click=${this._createSection}
|
||||||
|
aria-label=${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.create_section"
|
||||||
)}
|
)}
|
||||||
</p>
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.lovelace.editor.section.create_section"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-ripple></ha-ripple>
|
||||||
|
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
</ha-sortable>
|
||||||
class="create-section"
|
`
|
||||||
@click=${this._createSection}
|
: nothing}
|
||||||
aria-label=${this.hass.localize(
|
</div>
|
||||||
"ui.panel.lovelace.editor.section.create_section"
|
</ha-sortable>
|
||||||
)}
|
${this._config?.sidebar
|
||||||
.title=${this.hass.localize(
|
? html`
|
||||||
"ui.panel.lovelace.editor.section.create_section"
|
<hui-view-sidebar
|
||||||
)}
|
class=${classMap({
|
||||||
>
|
"mobile-hidden": this.narrow && !this._showSidebar,
|
||||||
<ha-ripple></ha-ripple>
|
})}
|
||||||
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
|
.hass=${this.hass}
|
||||||
</button>
|
.badges=${this.badges}
|
||||||
</div>
|
.lovelace=${this.lovelace}
|
||||||
</ha-sortable>
|
.viewIndex=${this.index}
|
||||||
`
|
.config=${this._config.sidebar}
|
||||||
: nothing}
|
></hui-view-sidebar>
|
||||||
${editMode && this._config?.cards?.length
|
`
|
||||||
? html`
|
: nothing}
|
||||||
<div class="section imported-cards">
|
</div>
|
||||||
<div class="imported-card-header">
|
<div class="imported-cards-section">
|
||||||
<p class="title">
|
${editMode && this._config?.cards?.length
|
||||||
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
|
? html`
|
||||||
${this.hass.localize(
|
<div class="section imported-cards">
|
||||||
"ui.panel.lovelace.editor.section.imported_cards_title"
|
<div class="imported-card-header">
|
||||||
)}
|
<p class="title">
|
||||||
</p>
|
<ha-svg-icon .path=${mdiEyeOff}></ha-svg-icon>
|
||||||
<p class="subtitle">
|
${this.hass.localize(
|
||||||
${this.hass.localize(
|
"ui.panel.lovelace.editor.section.imported_cards_title"
|
||||||
"ui.panel.lovelace.editor.section.imported_cards_description"
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<hui-section
|
|
||||||
.lovelace=${this.lovelace}
|
|
||||||
.hass=${this.hass}
|
|
||||||
.config=${this._importedCardSectionConfig(
|
|
||||||
this._config.cards
|
|
||||||
)}
|
)}
|
||||||
.viewIndex=${this.index}
|
</p>
|
||||||
preview
|
<p class="subtitle">
|
||||||
import-only
|
${this.hass.localize(
|
||||||
></hui-section>
|
"ui.panel.lovelace.editor.section.imported_cards_description"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
`
|
<hui-section
|
||||||
: nothing}
|
.lovelace=${this.lovelace}
|
||||||
</div>
|
.hass=${this.hass}
|
||||||
</ha-sortable>
|
.config=${this._importedCardSectionConfig(
|
||||||
|
this._config.cards
|
||||||
|
)}
|
||||||
|
.viewIndex=${this.index}
|
||||||
|
preview
|
||||||
|
import-only
|
||||||
|
></hui-section>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: nothing}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -352,6 +409,46 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
this.lovelace!.saveConfig(newConfig);
|
this.lovelace!.saveConfig(newConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _viewChanged(ev: CustomEvent) {
|
||||||
|
const newValue = ev.detail.value;
|
||||||
|
const shouldShowSidebar = newValue === "sidebar";
|
||||||
|
|
||||||
|
if (shouldShowSidebar !== this._showSidebar) {
|
||||||
|
this._toggleView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleView() {
|
||||||
|
// Save current scroll position
|
||||||
|
if (this._showSidebar) {
|
||||||
|
// Currently showing sidebar, save its scroll position
|
||||||
|
const sidebar = this.shadowRoot?.querySelector("hui-view-sidebar");
|
||||||
|
if (sidebar) {
|
||||||
|
this._sidebarScrollTop = sidebar.scrollTop || 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Currently showing content, save window scroll position
|
||||||
|
this._contentScrollTop = window.scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle view
|
||||||
|
this._showSidebar = !this._showSidebar;
|
||||||
|
|
||||||
|
// Restore scroll position after view updates
|
||||||
|
this.updateComplete.then(() => {
|
||||||
|
if (this._showSidebar) {
|
||||||
|
// Switched to sidebar, restore sidebar scroll
|
||||||
|
const sidebar = this.shadowRoot?.querySelector("hui-view-sidebar");
|
||||||
|
if (sidebar) {
|
||||||
|
sidebar.scrollTop = this._sidebarScrollTop;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Switched to content, restore window scroll
|
||||||
|
window.scrollTo(0, this._contentScrollTop);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static styles = css`
|
static styles = css`
|
||||||
:host {
|
:host {
|
||||||
--row-height: var(--ha-view-sections-row-height, 56px);
|
--row-height: var(--ha-view-sections-row-height, 56px);
|
||||||
@@ -369,14 +466,19 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper.top-margin {
|
.wrapper {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: var(--top-margin);
|
padding: var(--row-gap) var(--column-gap);
|
||||||
|
box-sizing: content-box;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: calc(
|
||||||
|
var(--column-count) * var(--column-max-width) +
|
||||||
|
(var(--column-count) - 1) * var(--column-gap)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.container > * {
|
.wrapper.top-margin {
|
||||||
position: relative;
|
margin-top: var(--top-margin);
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
@@ -390,22 +492,92 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
--column-count: min(var(--max-column-count), var(--total-section-count));
|
display: grid;
|
||||||
|
grid-template-columns: [content-start] repeat(
|
||||||
|
var(--content-column-count),
|
||||||
|
1fr
|
||||||
|
);
|
||||||
|
gap: var(--row-gap) var(--column-gap);
|
||||||
|
padding: var(--row-gap) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.has-sidebar .container {
|
||||||
|
grid-template-columns:
|
||||||
|
[content-start] repeat(var(--content-column-count), 1fr)
|
||||||
|
[sidebar-start] 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mobile with sidebar, content and sidebar both take full width */
|
||||||
|
.wrapper.narrow.has-sidebar .container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
hui-view-sidebar {
|
||||||
|
grid-column: sidebar-start / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper.narrow hui-view-sidebar {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
padding-bottom: calc(
|
||||||
|
var(--ha-space-4) + 56px + var(--ha-space-4) +
|
||||||
|
env(safe-area-inset-bottom)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tabs {
|
||||||
|
position: fixed;
|
||||||
|
bottom: calc(var(--ha-space-4) + env(safe-area-inset-bottom));
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
padding: 0;
|
||||||
|
z-index: 1;
|
||||||
|
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.15))
|
||||||
|
drop-shadow(0 4px 16px rgba(0, 0, 0, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-tabs ha-control-select {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 280px;
|
||||||
|
max-width: 90%;
|
||||||
|
--control-select-thickness: 56px;
|
||||||
|
--control-select-border-radius: var(--ha-border-radius-6xl);
|
||||||
|
--control-select-background: var(--card-background-color);
|
||||||
|
--control-select-background-opacity: 1;
|
||||||
|
--control-select-color: var(--primary-color);
|
||||||
|
--control-select-padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ha-sortable {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
grid-column: content-start / sidebar-start;
|
||||||
|
grid-row: 1 / -1;
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
grid-template-columns: repeat(var(--column-count), 1fr);
|
grid-template-columns: repeat(var(--content-column-count), 1fr);
|
||||||
grid-auto-flow: row;
|
grid-auto-flow: row;
|
||||||
gap: var(--row-gap) var(--column-gap);
|
gap: var(--row-gap) var(--column-gap);
|
||||||
padding: var(--row-gap) var(--column-gap);
|
}
|
||||||
box-sizing: content-box;
|
|
||||||
margin: 0 auto;
|
.wrapper.narrow .content {
|
||||||
max-width: calc(
|
grid-column: 1 / -1;
|
||||||
var(--column-count) * var(--column-max-width) +
|
}
|
||||||
(var(--column-count) - 1) * var(--column-gap)
|
|
||||||
|
.wrapper.narrow.has-sidebar .content {
|
||||||
|
padding-bottom: calc(
|
||||||
|
var(--ha-space-4) + 56px + var(--ha-space-4) +
|
||||||
|
env(safe-area-inset-bottom)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
.container.dense {
|
|
||||||
|
.content.dense {
|
||||||
grid-auto-flow: row dense;
|
grid-auto-flow: row dense;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,13 +655,7 @@ export class SectionsView extends LitElement implements LovelaceViewElement {
|
|||||||
|
|
||||||
hui-view-header {
|
hui-view-header {
|
||||||
display: block;
|
display: block;
|
||||||
padding: 0 var(--column-gap);
|
|
||||||
padding-top: var(--row-gap);
|
padding-top: var(--row-gap);
|
||||||
margin: auto;
|
|
||||||
max-width: calc(
|
|
||||||
var(--max-column-count) * var(--column-max-width) +
|
|
||||||
(var(--max-column-count) - 1) * var(--column-gap)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.imported-cards {
|
.imported-cards {
|
||||||
|
|||||||
57
src/panels/lovelace/views/hui-view-sidebar.ts
Normal file
57
src/panels/lovelace/views/hui-view-sidebar.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { css, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { repeat } from "lit/directives/repeat";
|
||||||
|
import type { LovelaceViewSidebarConfig } from "../../../data/lovelace/config/view";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import "../sections/hui-section";
|
||||||
|
import type { Lovelace } from "../types";
|
||||||
|
|
||||||
|
export const DEFAULT_VIEW_SIDEBAR_LAYOUT = "start";
|
||||||
|
|
||||||
|
@customElement("hui-view-sidebar")
|
||||||
|
export class HuiViewSidebar extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public lovelace!: Lovelace;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public config?: LovelaceViewSidebarConfig;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public viewIndex!: number;
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (!this.lovelace) return nothing;
|
||||||
|
|
||||||
|
// Use preview mode instead of setting lovelace to avoid the sections to be
|
||||||
|
// editable as it is not yet supported
|
||||||
|
return html`
|
||||||
|
<div class="container">
|
||||||
|
${repeat(
|
||||||
|
this.config?.sections || [],
|
||||||
|
(section) => html`
|
||||||
|
<hui-section
|
||||||
|
.config=${section}
|
||||||
|
.hass=${this.hass}
|
||||||
|
.preview=${this.lovelace.editMode}
|
||||||
|
.viewIndex=${this.viewIndex}
|
||||||
|
></hui-section>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--row-gap, 8px);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hui-view-sidebar": HuiViewSidebar;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
import type { PropertyValues, TemplateResult } from "lit";
|
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 { customElement, property, state } from "lit/decorators";
|
||||||
|
import "../../components/ha-divider";
|
||||||
import "../../components/ha-list-item";
|
import "../../components/ha-list-item";
|
||||||
import "../../components/ha-select";
|
import "../../components/ha-select";
|
||||||
import "../../components/ha-settings-row";
|
import "../../components/ha-settings-row";
|
||||||
|
import { saveFrontendUserData } from "../../data/frontend";
|
||||||
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
|
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
|
||||||
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
||||||
import type { HomeAssistant } from "../../types";
|
import { getPanelTitle } from "../../data/panel";
|
||||||
import { saveFrontendUserData } from "../../data/frontend";
|
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||||
|
import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards";
|
||||||
|
|
||||||
const USE_SYSTEM_VALUE = "___use_system___";
|
const USE_SYSTEM_VALUE = "___use_system___";
|
||||||
|
|
||||||
@@ -47,12 +50,24 @@ class HaPickDashboardRow extends LitElement {
|
|||||||
<ha-list-item .value=${USE_SYSTEM_VALUE}>
|
<ha-list-item .value=${USE_SYSTEM_VALUE}>
|
||||||
${this.hass.localize("ui.panel.profile.dashboard.system")}
|
${this.hass.localize("ui.panel.profile.dashboard.system")}
|
||||||
</ha-list-item>
|
</ha-list-item>
|
||||||
|
<ha-divider></ha-divider>
|
||||||
<ha-list-item value="lovelace">
|
<ha-list-item value="lovelace">
|
||||||
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
|
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
|
||||||
</ha-list-item>
|
</ha-list-item>
|
||||||
<ha-list-item value="home">
|
${PANEL_DASHBOARDS.map((panel) => {
|
||||||
${this.hass.localize("ui.panel.profile.dashboard.home")}
|
const panelInfo = this.hass.panels[panel] as
|
||||||
</ha-list-item>
|
| 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) => {
|
${this._dashboards.map((dashboard) => {
|
||||||
if (!this.hass.user!.is_admin && dashboard.require_admin) {
|
if (!this.hass.user!.is_admin && dashboard.require_admin) {
|
||||||
return "";
|
return "";
|
||||||
|
|||||||
@@ -2217,7 +2217,9 @@
|
|||||||
"sidebar_toggle": "Sidebar toggle",
|
"sidebar_toggle": "Sidebar toggle",
|
||||||
"edit_sidebar": "Edit sidebar",
|
"edit_sidebar": "Edit sidebar",
|
||||||
"edit_subtitle": "Synced on all devices",
|
"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": {
|
"panel": {
|
||||||
"home": {
|
"home": {
|
||||||
@@ -3508,6 +3510,7 @@
|
|||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"add_dashboard": "Add dashboard",
|
"add_dashboard": "Add dashboard",
|
||||||
|
"set_as_default": "Set as default",
|
||||||
"type": {
|
"type": {
|
||||||
"user_created": "User created",
|
"user_created": "User created",
|
||||||
"built_in": "Built-in"
|
"built_in": "Built-in"
|
||||||
@@ -3516,7 +3519,7 @@
|
|||||||
"confirm_delete_title": "Delete {dashboard_title}?",
|
"confirm_delete_title": "Delete {dashboard_title}?",
|
||||||
"confirm_delete_text": "This dashboard will be permanently deleted.",
|
"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_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": {
|
"detail": {
|
||||||
"edit_dashboard": "Edit dashboard",
|
"edit_dashboard": "Edit dashboard",
|
||||||
"new_dashboard": "Add new dashboard",
|
"new_dashboard": "Add new dashboard",
|
||||||
@@ -3533,9 +3536,7 @@
|
|||||||
"set_default": "Set as default",
|
"set_default": "Set as default",
|
||||||
"remove_default": "Remove as default",
|
"remove_default": "Remove as default",
|
||||||
"set_default_confirm_title": "Set as default dashboard?",
|
"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.",
|
"set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant. Each user can change this in their profile."
|
||||||
"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."
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"resources": {
|
"resources": {
|
||||||
@@ -6780,6 +6781,7 @@
|
|||||||
},
|
},
|
||||||
"analytics": {
|
"analytics": {
|
||||||
"caption": "Analytics",
|
"caption": "Analytics",
|
||||||
|
"header": "Home Assistant analytics",
|
||||||
"description": "Learn how to share data to improve Home Assistant",
|
"description": "Learn how to share data to improve Home Assistant",
|
||||||
"preferences": {
|
"preferences": {
|
||||||
"base": {
|
"base": {
|
||||||
@@ -6797,10 +6799,21 @@
|
|||||||
"diagnostics": {
|
"diagnostics": {
|
||||||
"title": "Diagnostics",
|
"title": "Diagnostics",
|
||||||
"description": "Share crash reports when unexpected errors occur."
|
"description": "Share crash reports when unexpected errors occur."
|
||||||
|
},
|
||||||
|
"snapshots": {
|
||||||
|
"title": "Devices",
|
||||||
|
"description": "Generic information about your devices.",
|
||||||
|
"header": "Device analytics",
|
||||||
|
"info": "Anonymously share data about your devices to help build the Open Home 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",
|
"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.",
|
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
|
||||||
"download_device_info": "Preview device analytics"
|
"download_device_info": "Preview device analytics"
|
||||||
},
|
},
|
||||||
@@ -7059,7 +7072,9 @@
|
|||||||
"unamed_device": "Unnamed device",
|
"unamed_device": "Unnamed device",
|
||||||
"others": "Others",
|
"others": "Others",
|
||||||
"scenes": "Scenes",
|
"scenes": "Scenes",
|
||||||
"automations": "Automations"
|
"automations": "Automations",
|
||||||
|
"for_you": "For you",
|
||||||
|
"home": "Home"
|
||||||
},
|
},
|
||||||
"common_controls": {
|
"common_controls": {
|
||||||
"not_loaded": "Usage Prediction integration is not loaded.",
|
"not_loaded": "Usage Prediction integration is not loaded.",
|
||||||
@@ -7146,6 +7161,7 @@
|
|||||||
"energy_usage_graph": {
|
"energy_usage_graph": {
|
||||||
"total_consumed": "Total consumed {num} kWh",
|
"total_consumed": "Total consumed {num} kWh",
|
||||||
"total_returned": "Total returned {num} kWh",
|
"total_returned": "Total returned {num} kWh",
|
||||||
|
"total_usage": "{num} kWh used",
|
||||||
"combined_from_grid": "Combined from grid",
|
"combined_from_grid": "Combined from grid",
|
||||||
"consumed_solar": "Consumed solar",
|
"consumed_solar": "Consumed solar",
|
||||||
"consumed_battery": "Consumed battery"
|
"consumed_battery": "Consumed battery"
|
||||||
@@ -7329,6 +7345,8 @@
|
|||||||
"header": "View configuration",
|
"header": "View configuration",
|
||||||
"header_name": "{name} view configuration",
|
"header_name": "{name} view configuration",
|
||||||
"add": "Add view",
|
"add": "Add view",
|
||||||
|
"show_sidebar": "Show sidebar",
|
||||||
|
"show_content": "Show content",
|
||||||
"background": {
|
"background": {
|
||||||
"settings": "Background settings",
|
"settings": "Background settings",
|
||||||
"image": "Background image",
|
"image": "Background image",
|
||||||
|
|||||||
10
yarn.lock
10
yarn.lock
@@ -9332,7 +9332,7 @@ __metadata:
|
|||||||
gulp-rename: "npm:2.1.0"
|
gulp-rename: "npm:2.1.0"
|
||||||
gulp-zopfli-green: "npm:6.0.2"
|
gulp-zopfli-green: "npm:6.0.2"
|
||||||
hls.js: "npm:1.6.14"
|
hls.js: "npm:1.6.14"
|
||||||
home-assistant-js-websocket: "npm:9.5.0"
|
home-assistant-js-websocket: "npm:9.6.0"
|
||||||
html-minifier-terser: "npm:7.2.0"
|
html-minifier-terser: "npm:7.2.0"
|
||||||
husky: "npm:9.1.7"
|
husky: "npm:9.1.7"
|
||||||
idb-keyval: "npm:6.2.2"
|
idb-keyval: "npm:6.2.2"
|
||||||
@@ -9393,10 +9393,10 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"home-assistant-js-websocket@npm:9.5.0":
|
"home-assistant-js-websocket@npm:9.6.0":
|
||||||
version: 9.5.0
|
version: 9.6.0
|
||||||
resolution: "home-assistant-js-websocket@npm:9.5.0"
|
resolution: "home-assistant-js-websocket@npm:9.6.0"
|
||||||
checksum: 10/42f991b3b85aa61be28984f099a001ac083fb3da54b2777283d0c97976c564a303d8d4ba467e1b8e29cbc33151cd6eef64c1a7d3392d62bbb9cbb27aa7ca9942
|
checksum: 10/0eded7864632b5e19e92289ffac0e24308b1e8f425e292ae87ed21450852f7705db521e202614b1d5bbdb7948633143dce2524ed548db0c38486b40ed1ffa474
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user