mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-26 11:17:21 +00:00
Compare commits
85 Commits
helpers-en
...
add-automa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1289a6cfdc | ||
|
|
252de4dea4 | ||
|
|
39bc4c5b7f | ||
|
|
5fe89ab8dc | ||
|
|
c74d59dfc6 | ||
|
|
6c52b2252e | ||
|
|
e7edb1ba80 | ||
|
|
161ccc752c | ||
|
|
fe50c1212a | ||
|
|
c01fbf5f47 | ||
|
|
1a6d1580a2 | ||
|
|
5fde08bc47 | ||
|
|
bf36d4477e | ||
|
|
5c8da28b61 | ||
|
|
bbb3c0208b | ||
|
|
2a6e48bc23 | ||
|
|
f9f9622873 | ||
|
|
82a3db39fe | ||
|
|
254857b53f | ||
|
|
1648be6b83 | ||
|
|
c4c774c217 | ||
|
|
6a0aab2088 | ||
|
|
f89caaf1a5 | ||
|
|
a3f0e3507b | ||
|
|
7ac34f424f | ||
|
|
5a5593ec5b | ||
|
|
2d39afdeac | ||
|
|
974ac31277 | ||
|
|
47e98d532d | ||
|
|
2efc513221 | ||
|
|
5d820e3046 | ||
|
|
c3cff3bcd3 | ||
|
|
e65a8a6b66 | ||
|
|
e041658ca1 | ||
|
|
d410363b81 | ||
|
|
fa58b07801 | ||
|
|
0b89f7af12 | ||
|
|
cb7f35979a | ||
|
|
1dbdad5605 | ||
|
|
75a127d453 | ||
|
|
5c15354302 | ||
|
|
fedcf85264 | ||
|
|
4b9a716df7 | ||
|
|
e319d116dc | ||
|
|
c8b1fd074c | ||
|
|
ec9bf7dd94 | ||
|
|
6d01949ff5 | ||
|
|
8a8d7dc77a | ||
|
|
5fc629f5c5 | ||
|
|
d070754bb9 | ||
|
|
a2a0f900a8 | ||
|
|
b18d3bff21 | ||
|
|
b06b51d03d | ||
|
|
022343c113 | ||
|
|
5ca10f4a38 | ||
|
|
33af5b6a08 | ||
|
|
0c89415c1c | ||
|
|
bd6b2c685c | ||
|
|
cdb37edc5f | ||
|
|
d68b1571ae | ||
|
|
a274727713 | ||
|
|
58a1aac131 | ||
|
|
4cdbda25ed | ||
|
|
618c6e1ef2 | ||
|
|
10b308b906 | ||
|
|
563c6f0fbf | ||
|
|
1dc7f0ab73 | ||
|
|
5c8954b714 | ||
|
|
e506e4b082 | ||
|
|
9628d69998 | ||
|
|
350d8e7a49 | ||
|
|
61a5b607e0 | ||
|
|
2f3a2b8418 | ||
|
|
12c1e4eec4 | ||
|
|
b704b621f2 | ||
|
|
0eb993a8df | ||
|
|
cda97766ac | ||
|
|
e6936a9294 | ||
|
|
5ad73287a2 | ||
|
|
591b464508 | ||
|
|
acf963d38c | ||
|
|
68f383c293 | ||
|
|
0a25d8106c | ||
|
|
5c3cf17df9 | ||
|
|
e905fa6f23 |
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.19",
|
||||
"@fullcalendar/luxon3": "6.1.19",
|
||||
"@fullcalendar/timegrid": "6.1.19",
|
||||
"@home-assistant/webawesome": "3.0.0",
|
||||
"@home-assistant/webawesome": "3.0.0-ha.0",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.0.9",
|
||||
"@lit-labs/observers": "2.0.6",
|
||||
@@ -112,7 +112,7 @@
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"hls.js": "1.6.14",
|
||||
"home-assistant-js-websocket": "9.5.0",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
"intl-messageformat": "10.7.18",
|
||||
"js-yaml": "4.1.1",
|
||||
|
||||
84
src/components/ha-condition-icon.ts
Normal file
84
src/components/ha-condition-icon.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import {
|
||||
mdiAmpersand,
|
||||
mdiClockOutline,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiGateOr,
|
||||
mdiIdentifier,
|
||||
mdiMapMarkerRadius,
|
||||
mdiNotEqualVariant,
|
||||
mdiNumeric,
|
||||
mdiStateMachine,
|
||||
mdiWeatherSunny,
|
||||
} from "@mdi/js";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { conditionIcon, FALLBACK_DOMAIN_ICONS } from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
export const CONDITION_ICONS = {
|
||||
device: mdiDevices,
|
||||
and: mdiAmpersand,
|
||||
or: mdiGateOr,
|
||||
not: mdiNotEqualVariant,
|
||||
state: mdiStateMachine,
|
||||
numeric_state: mdiNumeric,
|
||||
sun: mdiWeatherSunny,
|
||||
template: mdiCodeBraces,
|
||||
time: mdiClockOutline,
|
||||
trigger: mdiIdentifier,
|
||||
zone: mdiMapMarkerRadius,
|
||||
};
|
||||
|
||||
@customElement("ha-condition-icon")
|
||||
export class HaConditionIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public condition?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
}
|
||||
|
||||
if (!this.condition) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = conditionIcon(this.hass, this.condition).then((icn) => {
|
||||
if (icn) {
|
||||
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||
}
|
||||
return this._renderFallback();
|
||||
});
|
||||
|
||||
return html`${until(icon)}`;
|
||||
}
|
||||
|
||||
private _renderFallback() {
|
||||
const domain = computeDomain(this.condition!);
|
||||
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
.path=${CONDITION_ICONS[this.condition!] ||
|
||||
FALLBACK_DOMAIN_ICONS[domain]}
|
||||
></ha-svg-icon>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-condition-icon": HaConditionIcon;
|
||||
}
|
||||
}
|
||||
@@ -75,11 +75,15 @@ export class HaDialogHeader extends LitElement {
|
||||
font-size: var(--ha-font-size-xl);
|
||||
line-height: var(--ha-line-height-condensed);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
color: var(--ha-dialog-header-title-color, var(--primary-text-color));
|
||||
}
|
||||
.header-subtitle {
|
||||
font-size: var(--ha-font-size-m);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
color: var(--secondary-text-color);
|
||||
color: var(
|
||||
--ha-dialog-header-subtitle-color,
|
||||
var(--secondary-text-color)
|
||||
);
|
||||
}
|
||||
@media all and (min-width: 450px) and (min-height: 500px) {
|
||||
.header-bar {
|
||||
|
||||
@@ -209,6 +209,7 @@ export class HaExpansionPanel extends LitElement {
|
||||
::slotted([slot="header"]) {
|
||||
flex: 1;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
.container {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
mdiHomeFloor3,
|
||||
mdiHomeFloorNegative1,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, html } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import type { FloorRegistryEntry } from "../data/floor_registry";
|
||||
import "./ha-icon";
|
||||
@@ -48,7 +48,7 @@ export const floorDefaultIcon = (floor: Pick<FloorRegistryEntry, "level">) => {
|
||||
|
||||
@customElement("ha-floor-icon")
|
||||
export class HaFloorIcon extends LitElement {
|
||||
@property({ attribute: false }) public floor!: Pick<
|
||||
@property({ attribute: false }) public floor?: Pick<
|
||||
FloorRegistryEntry,
|
||||
"icon" | "level"
|
||||
>;
|
||||
@@ -56,6 +56,9 @@ export class HaFloorIcon extends LitElement {
|
||||
@property() public icon?: string;
|
||||
|
||||
protected render() {
|
||||
if (!this.floor) {
|
||||
return nothing;
|
||||
}
|
||||
if (this.floor.icon) {
|
||||
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
cursor: initial;
|
||||
}
|
||||
div[role="separator"] {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface DisplayItem {
|
||||
label: string;
|
||||
description?: string;
|
||||
disableSorting?: boolean;
|
||||
disableHiding?: boolean;
|
||||
}
|
||||
|
||||
export interface DisplayValue {
|
||||
@@ -101,6 +102,7 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
icon,
|
||||
iconPath,
|
||||
disableSorting,
|
||||
disableHiding,
|
||||
} = item;
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@@ -155,7 +157,8 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
${!isVisible || !disableHiding
|
||||
? html`<ha-icon-button
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="end"
|
||||
.label=${this.hass.localize(
|
||||
@@ -166,7 +169,9 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
)}
|
||||
.value=${value}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
.disabled=${disableHiding || false}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${isVisible && !disableSorting
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
|
||||
@@ -154,7 +154,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
return this._getLabelsMemoized(
|
||||
this.hass,
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._labels,
|
||||
this.includeDomains,
|
||||
this.excludeDomains,
|
||||
|
||||
@@ -36,6 +36,11 @@ export class HaMdMenuItem extends MenuItemEl {
|
||||
::slotted([slot="headline"]) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 1;
|
||||
--md-menu-item-label-text-color: var(--disabled-text-color);
|
||||
--md-menu-item-leading-icon-color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
||||
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
@@ -43,13 +43,8 @@ const createViewNavigationItem = (
|
||||
|
||||
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||
path: `/${panel.url_path}`,
|
||||
icon: panel.icon ?? "mdi:view-dashboard",
|
||||
title:
|
||||
panel.url_path === getDefaultPanelUrlPath(hass)
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title ||
|
||||
(panel.url_path ? titleCase(panel.url_path) : ""),
|
||||
icon: getPanelIcon(panel) || "mdi:view-dashboard",
|
||||
title: getPanelTitle(hass, panel) || "",
|
||||
});
|
||||
|
||||
@customElement("ha-navigation-picker")
|
||||
|
||||
28
src/components/ha-section-title.ts
Normal file
28
src/components/ha-section-title.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
|
||||
@customElement("ha-section-title")
|
||||
class HaSectionTitle extends LitElement {
|
||||
protected render() {
|
||||
return html`<slot></slot>`;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
background-color: var(--ha-color-fill-neutral-quiet-resting);
|
||||
padding: var(--ha-space-1) var(--ha-space-2);
|
||||
font-weight: var(--ha-font-weight-bold);
|
||||
color: var(--secondary-text-color);
|
||||
min-height: var(--ha-space-6);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-section-title": HaSectionTitle;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +1,13 @@
|
||||
import {
|
||||
mdiBell,
|
||||
mdiCalendar,
|
||||
mdiCellphoneCog,
|
||||
mdiChartBox,
|
||||
mdiClipboardList,
|
||||
mdiCog,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiLightningBolt,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiTooltipAccount,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -33,7 +24,14 @@ import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import {
|
||||
FIXED_PANELS,
|
||||
getDefaultPanelUrlPath,
|
||||
getPanelIcon,
|
||||
getPanelIconPath,
|
||||
getPanelTitle,
|
||||
SHOW_AFTER_SPACER_PANELS,
|
||||
} from "../data/panel";
|
||||
import type { PersistentNotification } from "../data/persistent_notification";
|
||||
import { subscribeNotifications } from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
@@ -54,8 +52,6 @@ import "./ha-spinner";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
const SORT_VALUE_URL_PATHS = {
|
||||
@@ -67,18 +63,6 @@ const SORT_VALUE_URL_PATHS = {
|
||||
config: 11,
|
||||
};
|
||||
|
||||
export const PANEL_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
lovelace: mdiViewDashboard,
|
||||
map: mdiTooltipAccount,
|
||||
"media-browser": mdiPlayBoxMultiple,
|
||||
todo: mdiClipboardList,
|
||||
};
|
||||
|
||||
const panelSorter = (
|
||||
reverseSort: string[],
|
||||
defaultPanel: string,
|
||||
@@ -155,16 +139,23 @@ export const computePanels = memoizeOne(
|
||||
const beforeSpacer: PanelInfo[] = [];
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
const allPanels = Object.values(panels).filter(
|
||||
(panel) => !FIXED_PANELS.includes(panel.url_path)
|
||||
);
|
||||
|
||||
allPanels.forEach((panel) => {
|
||||
const isDefaultPanel = panel.url_path === defaultPanel;
|
||||
|
||||
if (
|
||||
!isDefaultPanel &&
|
||||
(!panel.title ||
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(!panel.title && panel.url_path !== defaultPanel) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path))
|
||||
!panelsOrder.includes(panel.url_path)))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
(SHOW_AFTER_SPACER_PANELS.includes(panel.url_path)
|
||||
? afterSpacer
|
||||
: beforeSpacer
|
||||
).push(panel);
|
||||
@@ -251,10 +242,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Show the supervisor as being part of configuration
|
||||
const selectedPanel = this.route.path?.startsWith("/hassio/")
|
||||
? "config"
|
||||
: this.hass.panelUrl;
|
||||
const selectedPanel = this.hass.panelUrl;
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
@@ -397,9 +385,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
private _renderAllPanels(selectedPanel: string) {
|
||||
if (!this._panelOrder || !this._hiddenPanels) {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}
|
||||
><ha-spinner size="small"></ha-spinner
|
||||
></ha-fade-in>
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="small"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -413,7 +401,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<ha-md-list
|
||||
class="ha-scrollbar"
|
||||
@@ -422,52 +409,33 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderExternalConfiguration()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this.hass.user?.is_admin
|
||||
? this._renderConfiguration(selectedPanel)
|
||||
: this._renderExternalConfiguration()}
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanels(
|
||||
panels: PanelInfo[],
|
||||
selectedPanel: string,
|
||||
defaultPanel: string
|
||||
) {
|
||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
panel.url_path === defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
: undefined,
|
||||
selectedPanel
|
||||
)
|
||||
this._renderPanel(panel, panel.url_path === selectedPanel)
|
||||
);
|
||||
}
|
||||
|
||||
private _renderPanel(
|
||||
urlPath: string,
|
||||
title: string | null,
|
||||
icon: string | null | undefined,
|
||||
iconPath: string | null | undefined,
|
||||
selectedPanel: string
|
||||
) {
|
||||
return urlPath === "config"
|
||||
? this._renderConfiguration(title, selectedPanel)
|
||||
: html`
|
||||
private _renderPanel(panel: PanelInfo, isSelected: boolean) {
|
||||
const title = getPanelTitle(this.hass, panel);
|
||||
const urlPath = panel.url_path;
|
||||
const icon = getPanelIcon(panel);
|
||||
const iconPath = getPanelIconPath(panel);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
.href=${`/${urlPath}`}
|
||||
type="link"
|
||||
class=${classMap({
|
||||
selected: selectedPanel === urlPath,
|
||||
})}
|
||||
class=${classMap({ selected: isSelected })}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
@@ -487,10 +455,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return html`<div class="spacer" disabled></div>`;
|
||||
}
|
||||
|
||||
private _renderConfiguration(title: string | null, selectedPanel: string) {
|
||||
private _renderConfiguration(selectedPanel: string) {
|
||||
if (!this.hass.user?.is_admin) {
|
||||
return nothing;
|
||||
}
|
||||
const isSelected =
|
||||
selectedPanel === "config" || this.route.path?.startsWith("/hassio/");
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
class="configuration${selectedPanel === "config" ? " selected" : ""}"
|
||||
class="configuration ${classMap({ selected: isSelected })}"
|
||||
type="button"
|
||||
href="/config"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -504,15 +477,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
${this._updatesCount + this._issuesCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("panel.config")}</span
|
||||
>
|
||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
@@ -535,19 +510,20 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
? html`
|
||||
<span class="badge" slot="start"> ${notificationCount} </span>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.notification_drawer.title")}</span
|
||||
>
|
||||
${this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderUserItem(selectedPanel: string) {
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isSelected = selectedPanel === "profile";
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@@ -555,7 +531,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
type="link"
|
||||
class=${classMap({
|
||||
user: true,
|
||||
selected: selectedPanel === "profile",
|
||||
selected: isSelected,
|
||||
rtl: isRTL,
|
||||
})}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -566,18 +542,18 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
.user=${this.hass.user}
|
||||
.hass=${this.hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.user ? this.hass.user.name : ""}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${!this.hass.user?.is_admin &&
|
||||
this.hass.auth.external?.config.hasSettingsScreen
|
||||
? html`
|
||||
if (!this.hass.auth.external?.config.hasSettingsScreen) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
type="button"
|
||||
@@ -589,8 +565,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: ""}`;
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleExternalAppConfiguration(ev: Event) {
|
||||
|
||||
178
src/components/ha-snowflakes.ts
Normal file
178
src/components/ha-snowflakes.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { subscribeLabFeatures } from "../data/labs";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
|
||||
interface Snowflake {
|
||||
id: number;
|
||||
left: number;
|
||||
size: number;
|
||||
duration: number;
|
||||
delay: number;
|
||||
blur: number;
|
||||
}
|
||||
|
||||
@customElement("ha-snowflakes")
|
||||
export class HaSnowflakes extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass?: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _enabled = false;
|
||||
|
||||
@state() private _snowflakes: Snowflake[] = [];
|
||||
|
||||
private _maxSnowflakes = 50;
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeLabFeatures(this.hass!.connection, (features) => {
|
||||
this._enabled =
|
||||
features.find(
|
||||
(f) =>
|
||||
f.domain === "frontend" && f.preview_feature === "winter_mode"
|
||||
)?.enabled ?? false;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
private _generateSnowflakes() {
|
||||
if (!this._enabled) {
|
||||
this._snowflakes = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const snowflakes: Snowflake[] = [];
|
||||
for (let i = 0; i < this._maxSnowflakes; i++) {
|
||||
snowflakes.push({
|
||||
id: i,
|
||||
left: Math.random() * 100, // Random position from 0-100%
|
||||
size: Math.random() * 12 + 8, // Random size between 8-20px
|
||||
duration: Math.random() * 8 + 8, // Random duration between 8-16s
|
||||
delay: Math.random() * 8, // Random delay between 0-8s
|
||||
blur: Math.random() * 1, // Random blur between 0-1px
|
||||
});
|
||||
}
|
||||
this._snowflakes = snowflakes;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: Map<string, unknown>) {
|
||||
super.willUpdate(changedProps);
|
||||
if (changedProps.has("_enabled")) {
|
||||
this._generateSnowflakes();
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._enabled) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const isDark = this.hass?.themes.darkMode ?? false;
|
||||
|
||||
return html`
|
||||
<div class="snowflakes ${isDark ? "dark" : "light"}" aria-hidden="true">
|
||||
${this._snowflakes.map(
|
||||
(flake) => html`
|
||||
<div
|
||||
class="snowflake ${this.narrow && flake.id >= 30
|
||||
? "hide-narrow"
|
||||
: ""}"
|
||||
style="
|
||||
left: ${flake.left}%;
|
||||
font-size: ${flake.size}px;
|
||||
animation-duration: ${flake.duration}s;
|
||||
animation-delay: ${flake.delay}s;
|
||||
filter: blur(${flake.blur}px);
|
||||
"
|
||||
>
|
||||
❄
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
static readonly styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.snowflakes {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 110%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.snowflake {
|
||||
position: absolute;
|
||||
top: -10%;
|
||||
opacity: 0.7;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
animation: fall linear infinite;
|
||||
}
|
||||
|
||||
.light .snowflake {
|
||||
color: #00bcd4;
|
||||
text-shadow:
|
||||
0 0 5px #00bcd4,
|
||||
0 0 10px #00e5ff;
|
||||
}
|
||||
|
||||
.dark .snowflake {
|
||||
color: #fff;
|
||||
text-shadow:
|
||||
0 0 5px rgba(255, 255, 255, 0.8),
|
||||
0 0 10px rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.snowflake.hide-narrow {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@keyframes fall {
|
||||
0% {
|
||||
transform: translateY(-10vh) translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateY(30vh) translateX(10px);
|
||||
}
|
||||
50% {
|
||||
transform: translateY(60vh) translateX(-10px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(85vh) translateX(10px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(120vh) translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.snowflake {
|
||||
animation: none;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-snowflakes": HaSnowflakes;
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
areaMeetsFilter,
|
||||
deviceMeetsFilter,
|
||||
entityRegMeetsFilter,
|
||||
getTargetComboBoxItemType,
|
||||
type TargetType,
|
||||
type TargetTypeFloorless,
|
||||
} from "../data/target";
|
||||
@@ -47,7 +48,6 @@ import "./ha-tree-indicator";
|
||||
import "./target-picker/ha-target-picker-item-group";
|
||||
import "./target-picker/ha-target-picker-value-chip";
|
||||
|
||||
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
|
||||
const SEPARATOR = "________";
|
||||
const CREATE_ID = "___create-new-entity___";
|
||||
|
||||
@@ -634,35 +634,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _getRowType = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
if (
|
||||
(item as FloorComboBoxItem).type === "area" ||
|
||||
(item as FloorComboBoxItem).type === "floor"
|
||||
) {
|
||||
return (item as FloorComboBoxItem).type;
|
||||
}
|
||||
|
||||
if ("domain" in item) {
|
||||
return "device";
|
||||
}
|
||||
|
||||
if ("stateObj" in item) {
|
||||
return "entity";
|
||||
}
|
||||
|
||||
if (item.id === EMPTY_SEARCH) {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return "label";
|
||||
};
|
||||
|
||||
private _sectionTitleFunction = ({
|
||||
firstIndex,
|
||||
lastIndex,
|
||||
@@ -686,7 +657,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const type = this._getRowType(firstItem as PickerComboBoxItem);
|
||||
const type = getTargetComboBoxItemType(firstItem as PickerComboBoxItem);
|
||||
const translationType:
|
||||
| "areas"
|
||||
| "entities"
|
||||
@@ -858,7 +829,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
|
||||
if (!filterType || filterType === "label") {
|
||||
let labels = this._getLabelsMemoized(
|
||||
this.hass,
|
||||
this.hass.states,
|
||||
this.hass.areas,
|
||||
this.hass.devices,
|
||||
this.hass.entities,
|
||||
this._labelRegistry,
|
||||
includeDomains,
|
||||
undefined,
|
||||
@@ -974,7 +948,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const type = this._getRowType(item);
|
||||
const type = getTargetComboBoxItemType(item);
|
||||
let hasFloor = false;
|
||||
let rtl = false;
|
||||
let showEntityId = false;
|
||||
|
||||
@@ -235,7 +235,7 @@ export class HaWaDialog extends LitElement {
|
||||
}
|
||||
|
||||
:host([width="large"]) wa-dialog {
|
||||
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
|
||||
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
|
||||
}
|
||||
|
||||
:host([width="full"]) wa-dialog {
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface AnalyticsPreferences {
|
||||
diagnostics?: boolean;
|
||||
usage?: boolean;
|
||||
statistics?: boolean;
|
||||
snapshots?: boolean;
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
|
||||
@@ -21,11 +21,52 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
|
||||
area?: AreaRegistryEntry;
|
||||
}
|
||||
|
||||
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
|
||||
floor?: FloorRegistryEntry;
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
|
||||
areas: FloorComboBoxItem[];
|
||||
}
|
||||
|
||||
export interface AreaFloorValue {
|
||||
id: string;
|
||||
type: "floor" | "area";
|
||||
}
|
||||
|
||||
export const getAreasNestedInFloors = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
formatId: (value: AreaFloorValue) => string,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[]
|
||||
) =>
|
||||
getAreasAndFloorsItems(
|
||||
states,
|
||||
haFloors,
|
||||
haAreas,
|
||||
haDevices,
|
||||
haEntities,
|
||||
formatId,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
excludeFloors,
|
||||
true
|
||||
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
|
||||
|
||||
export const getAreasAndFloors = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
@@ -40,7 +81,43 @@ export const getAreasAndFloors = (
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[]
|
||||
): FloorComboBoxItem[] => {
|
||||
) =>
|
||||
getAreasAndFloorsItems(
|
||||
states,
|
||||
haFloors,
|
||||
haAreas,
|
||||
haDevices,
|
||||
haEntities,
|
||||
formatId,
|
||||
includeDomains,
|
||||
excludeDomains,
|
||||
includeDeviceClasses,
|
||||
deviceFilter,
|
||||
entityFilter,
|
||||
excludeAreas,
|
||||
excludeFloors
|
||||
) as FloorComboBoxItem[];
|
||||
|
||||
const getAreasAndFloorsItems = (
|
||||
states: HomeAssistant["states"],
|
||||
haFloors: HomeAssistant["floors"],
|
||||
haAreas: HomeAssistant["areas"],
|
||||
haDevices: HomeAssistant["devices"],
|
||||
haEntities: HomeAssistant["entities"],
|
||||
formatId: (value: AreaFloorValue) => string,
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
includeDeviceClasses?: string[],
|
||||
deviceFilter?: HaDevicePickerDeviceFilterFunc,
|
||||
entityFilter?: HaEntityPickerEntityFilterFunc,
|
||||
excludeAreas?: string[],
|
||||
excludeFloors?: string[],
|
||||
nested = false
|
||||
): (
|
||||
| FloorComboBoxItem
|
||||
| FloorNestedComboBoxItem
|
||||
| UnassignedAreasFloorComboBoxItem
|
||||
)[] => {
|
||||
const floors = Object.values(haFloors);
|
||||
const areas = Object.values(haAreas);
|
||||
const devices = Object.values(haDevices);
|
||||
@@ -181,7 +258,11 @@ export const getAreasAndFloors = (
|
||||
|
||||
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
|
||||
|
||||
const items: FloorComboBoxItem[] = [];
|
||||
const items: (
|
||||
| FloorComboBoxItem
|
||||
| FloorNestedComboBoxItem
|
||||
| UnassignedAreasFloorComboBoxItem
|
||||
)[] = [];
|
||||
|
||||
hierarchy.floors.forEach((f) => {
|
||||
const floor = haFloors[f.id];
|
||||
@@ -196,7 +277,7 @@ export const getAreasAndFloors = (
|
||||
})
|
||||
.flat();
|
||||
|
||||
items.push({
|
||||
const floorItem: FloorComboBoxItem | FloorNestedComboBoxItem = {
|
||||
id: formatId({ id: floor.floor_id, type: "floor" }),
|
||||
type: "floor",
|
||||
primary: floorName,
|
||||
@@ -208,10 +289,11 @@ export const getAreasAndFloors = (
|
||||
...floor.aliases,
|
||||
...areaSearchLabels,
|
||||
],
|
||||
});
|
||||
};
|
||||
|
||||
items.push(
|
||||
...floorAreas.map((area) => {
|
||||
items.push(floorItem);
|
||||
|
||||
const floorAreasItems = floorAreas.map((area) => {
|
||||
const areaName = computeAreaName(area);
|
||||
return {
|
||||
id: formatId({ id: area.area_id, type: "area" }),
|
||||
@@ -225,12 +307,16 @@ export const getAreasAndFloors = (
|
||||
...area.aliases,
|
||||
],
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
items.push(
|
||||
...hierarchy.areas.map((areaId) => {
|
||||
if (nested && floor) {
|
||||
(floorItem as unknown as FloorNestedComboBoxItem).areas = floorAreasItems;
|
||||
} else {
|
||||
items.push(...floorAreasItems);
|
||||
}
|
||||
});
|
||||
|
||||
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
|
||||
const area = haAreas[areaId];
|
||||
const areaName = computeAreaName(area) || area.area_id;
|
||||
return {
|
||||
@@ -241,8 +327,15 @@ export const getAreasAndFloors = (
|
||||
icon: area.icon || undefined,
|
||||
search_labels: [area.area_id, areaName, ...area.aliases],
|
||||
};
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
if (nested && unassignedAreaItems.length) {
|
||||
items.push({
|
||||
areas: unassignedAreaItems,
|
||||
} as UnassignedAreasFloorComboBoxItem);
|
||||
} else {
|
||||
items.push(...unassignedAreaItems);
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { stringCompare } from "../common/string/compare";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { DeviceRegistryEntry } from "./device_registry";
|
||||
import type { EntityRegistryEntry } from "./entity_registry";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
EntityRegistryEntry,
|
||||
} from "./entity_registry";
|
||||
import type { RegistryEntry } from "./registry";
|
||||
|
||||
export { subscribeAreaRegistry } from "./ws-area_registry";
|
||||
@@ -18,7 +21,10 @@ export interface AreaRegistryEntry extends RegistryEntry {
|
||||
temperature_entity_id: string | null;
|
||||
}
|
||||
|
||||
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
|
||||
export type AreaEntityLookup = Record<
|
||||
string,
|
||||
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
|
||||
>;
|
||||
|
||||
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
|
||||
|
||||
@@ -69,11 +75,17 @@ export const reorderAreaRegistryEntries = (
|
||||
});
|
||||
|
||||
export const getAreaEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
|
||||
filterHidden = false
|
||||
): AreaEntityLookup => {
|
||||
const areaEntityLookup: AreaEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.area_id) {
|
||||
if (
|
||||
!entity.area_id ||
|
||||
(filterHidden &&
|
||||
((entity as EntityRegistryDisplayEntry).hidden ||
|
||||
(entity as EntityRegistryEntry).hidden_by))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.area_id in areaEntityLookup)) {
|
||||
|
||||
@@ -214,6 +214,8 @@ export interface PipelineRun {
|
||||
stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error";
|
||||
run: PipelineRunStartEvent["data"];
|
||||
error?: PipelineErrorEvent["data"];
|
||||
started: Date;
|
||||
finished?: Date;
|
||||
wake_word?: PipelineWakeWordStartEvent["data"] &
|
||||
Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean };
|
||||
stt?: PipelineSTTStartEvent["data"] &
|
||||
@@ -235,6 +237,7 @@ export const processEvent = (
|
||||
stage: "ready",
|
||||
run: event.data,
|
||||
events: [event],
|
||||
started: new Date(event.timestamp),
|
||||
};
|
||||
return run;
|
||||
}
|
||||
@@ -290,9 +293,14 @@ export const processEvent = (
|
||||
tts: { ...run.tts!, ...event.data, done: true },
|
||||
};
|
||||
} else if (event.type === "run-end") {
|
||||
run = { ...run, stage: "done" };
|
||||
run = { ...run, finished: new Date(event.timestamp), stage: "done" };
|
||||
} else if (event.type === "error") {
|
||||
run = { ...run, stage: "error", error: event.data };
|
||||
run = {
|
||||
...run,
|
||||
finished: new Date(event.timestamp),
|
||||
stage: "error",
|
||||
error: event.data,
|
||||
};
|
||||
} else {
|
||||
run = { ...run };
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { LocalizeKeys } from "../common/translations/localize";
|
||||
import { createSearchParam } from "../common/url/search-params";
|
||||
import type { Context, HomeAssistant } from "../types";
|
||||
import type { BlueprintInput } from "./blueprint";
|
||||
import type { ConditionDescription } from "./condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "./condition";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import type { Action, Field, MODES } from "./script";
|
||||
@@ -236,6 +237,12 @@ interface BaseCondition {
|
||||
condition: string;
|
||||
alias?: string;
|
||||
enabled?: boolean;
|
||||
options?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface PlatformCondition extends BaseCondition {
|
||||
condition: Exclude<string, LegacyCondition["condition"]>;
|
||||
target?: HassServiceTarget;
|
||||
}
|
||||
|
||||
export interface LogicalCondition extends BaseCondition {
|
||||
@@ -320,7 +327,7 @@ export type AutomationElementGroup = Record<
|
||||
{ icon?: string; members?: AutomationElementGroup }
|
||||
>;
|
||||
|
||||
export type Condition =
|
||||
export type LegacyCondition =
|
||||
| StateCondition
|
||||
| NumericStateCondition
|
||||
| SunCondition
|
||||
@@ -331,6 +338,8 @@ export type Condition =
|
||||
| LogicalCondition
|
||||
| TriggerCondition;
|
||||
|
||||
export type Condition = LegacyCondition | PlatformCondition;
|
||||
|
||||
export type ConditionWithShorthand =
|
||||
| Condition
|
||||
| ShorthandAndConditionList
|
||||
@@ -608,6 +617,7 @@ export interface ConditionSidebarConfig extends BaseSidebarConfig {
|
||||
insertAfter: (value: Condition | Condition[]) => boolean;
|
||||
toggleYamlMode: () => void;
|
||||
config: Condition;
|
||||
description?: ConditionDescription;
|
||||
yamlMode: boolean;
|
||||
uiSupported: boolean;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,14 @@ import {
|
||||
} from "../common/string/format-list";
|
||||
import { hasTemplate } from "../common/string/has-template";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { Condition, ForDict, LegacyTrigger, Trigger } from "./automation";
|
||||
import type {
|
||||
Condition,
|
||||
ForDict,
|
||||
LegacyCondition,
|
||||
LegacyTrigger,
|
||||
Trigger,
|
||||
} from "./automation";
|
||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
|
||||
import {
|
||||
localizeDeviceAutomationCondition,
|
||||
@@ -896,6 +903,39 @@ const tryDescribeCondition = (
|
||||
}
|
||||
}
|
||||
|
||||
const description = describeLegacyCondition(
|
||||
condition as LegacyCondition,
|
||||
hass,
|
||||
entityRegistry
|
||||
);
|
||||
|
||||
if (description) {
|
||||
return description;
|
||||
}
|
||||
|
||||
const conditionType = condition.condition;
|
||||
|
||||
const domain = getConditionDomain(condition.condition);
|
||||
const type = getConditionObjectId(condition.condition);
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`component.${domain}.conditions.${type}.description_configured`
|
||||
) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${conditionType as LegacyCondition["condition"]}.label`
|
||||
) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.unknown_condition`
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const describeLegacyCondition = (
|
||||
condition: LegacyCondition,
|
||||
hass: HomeAssistant,
|
||||
entityRegistry: EntityRegistryEntry[]
|
||||
) => {
|
||||
if (condition.condition === "or") {
|
||||
const conditions = ensureArray(condition.conditions);
|
||||
|
||||
@@ -1287,12 +1327,5 @@ const tryDescribeCondition = (
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition.condition}.label`
|
||||
) ||
|
||||
hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.unknown_condition`
|
||||
)
|
||||
);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
228
src/data/chat_log.ts
Normal file
228
src/data/chat_log.ts
Normal file
@@ -0,0 +1,228 @@
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const enum ChatLogEventType {
|
||||
INITIAL_STATE = "initial_state",
|
||||
CREATED = "created",
|
||||
UPDATED = "updated",
|
||||
DELETED = "deleted",
|
||||
CONTENT_ADDED = "content_added",
|
||||
}
|
||||
|
||||
export interface ChatLogAttachment {
|
||||
media_content_id: string;
|
||||
mime_type: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ChatLogSystemContent {
|
||||
role: "system";
|
||||
content: string;
|
||||
created: Date;
|
||||
}
|
||||
|
||||
export interface ChatLogUserContent {
|
||||
role: "user";
|
||||
content: string;
|
||||
created: Date;
|
||||
attachments?: ChatLogAttachment[];
|
||||
}
|
||||
|
||||
export interface ChatLogAssistantContent {
|
||||
role: "assistant";
|
||||
agent_id: string;
|
||||
created: Date;
|
||||
content?: string;
|
||||
thinking_content?: string;
|
||||
tool_calls?: any[];
|
||||
}
|
||||
|
||||
export interface ChatLogToolResultContent {
|
||||
role: "tool_result";
|
||||
agent_id: string;
|
||||
tool_call_id: string;
|
||||
tool_name: string;
|
||||
tool_result: any;
|
||||
created: Date;
|
||||
}
|
||||
|
||||
export type ChatLogContent =
|
||||
| ChatLogSystemContent
|
||||
| ChatLogUserContent
|
||||
| ChatLogAssistantContent
|
||||
| ChatLogToolResultContent;
|
||||
|
||||
export interface ChatLog {
|
||||
conversation_id: string;
|
||||
continue_conversation: boolean;
|
||||
content: ChatLogContent[];
|
||||
created: Date;
|
||||
}
|
||||
|
||||
// Internal wire format types (not exported)
|
||||
interface ChatLogSystemContentWire {
|
||||
role: "system";
|
||||
content: string;
|
||||
created: string;
|
||||
}
|
||||
|
||||
interface ChatLogUserContentWire {
|
||||
role: "user";
|
||||
content: string;
|
||||
created: string;
|
||||
attachments?: ChatLogAttachment[];
|
||||
}
|
||||
|
||||
interface ChatLogAssistantContentWire {
|
||||
role: "assistant";
|
||||
agent_id: string;
|
||||
created: string;
|
||||
content?: string;
|
||||
thinking_content?: string;
|
||||
tool_calls?: {
|
||||
tool_name: string;
|
||||
tool_args: Record<string, any>;
|
||||
id: string;
|
||||
external: boolean;
|
||||
}[];
|
||||
}
|
||||
|
||||
interface ChatLogToolResultContentWire {
|
||||
role: "tool_result";
|
||||
agent_id: string;
|
||||
tool_call_id: string;
|
||||
tool_name: string;
|
||||
tool_result: any;
|
||||
created: string;
|
||||
}
|
||||
|
||||
type ChatLogContentWire =
|
||||
| ChatLogSystemContentWire
|
||||
| ChatLogUserContentWire
|
||||
| ChatLogAssistantContentWire
|
||||
| ChatLogToolResultContentWire;
|
||||
|
||||
interface ChatLogWire {
|
||||
conversation_id: string;
|
||||
continue_conversation: boolean;
|
||||
content: ChatLogContentWire[];
|
||||
created: string;
|
||||
}
|
||||
|
||||
const processContent = (content: ChatLogContentWire): ChatLogContent => ({
|
||||
...content,
|
||||
created: new Date(content.created),
|
||||
});
|
||||
|
||||
const processChatLog = (chatLog: ChatLogWire): ChatLog => ({
|
||||
...chatLog,
|
||||
created: new Date(chatLog.created),
|
||||
content: chatLog.content.map(processContent),
|
||||
});
|
||||
|
||||
interface ChatLogInitialStateEvent {
|
||||
event_type: ChatLogEventType.INITIAL_STATE;
|
||||
data: ChatLogWire;
|
||||
}
|
||||
|
||||
interface ChatLogIndexInitialStateEvent {
|
||||
event_type: ChatLogEventType.INITIAL_STATE;
|
||||
data: ChatLogWire[];
|
||||
}
|
||||
|
||||
interface ChatLogCreatedEvent {
|
||||
conversation_id: string;
|
||||
event_type: ChatLogEventType.CREATED;
|
||||
data: ChatLogWire;
|
||||
}
|
||||
|
||||
interface ChatLogUpdatedEvent {
|
||||
conversation_id: string;
|
||||
event_type: ChatLogEventType.UPDATED;
|
||||
data: { chat_log: ChatLogWire };
|
||||
}
|
||||
|
||||
interface ChatLogDeletedEvent {
|
||||
conversation_id: string;
|
||||
event_type: ChatLogEventType.DELETED;
|
||||
data: ChatLogWire;
|
||||
}
|
||||
|
||||
interface ChatLogContentAddedEvent {
|
||||
conversation_id: string;
|
||||
event_type: ChatLogEventType.CONTENT_ADDED;
|
||||
data: { content: ChatLogContentWire };
|
||||
}
|
||||
|
||||
type ChatLogSubscriptionEvent =
|
||||
| ChatLogInitialStateEvent
|
||||
| ChatLogUpdatedEvent
|
||||
| ChatLogDeletedEvent
|
||||
| ChatLogContentAddedEvent;
|
||||
|
||||
type ChatLogIndexSubscriptionEvent =
|
||||
| ChatLogIndexInitialStateEvent
|
||||
| ChatLogCreatedEvent
|
||||
| ChatLogDeletedEvent;
|
||||
|
||||
export const subscribeChatLog = (
|
||||
hass: HomeAssistant,
|
||||
conversationId: string,
|
||||
callback: (chatLog: ChatLog | null) => void
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
let chatLog: ChatLog | null = null;
|
||||
|
||||
return hass.connection.subscribeMessage<ChatLogSubscriptionEvent>(
|
||||
(event) => {
|
||||
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
|
||||
chatLog = processChatLog(event.data);
|
||||
callback(chatLog);
|
||||
} else if (event.event_type === ChatLogEventType.CONTENT_ADDED) {
|
||||
if (chatLog) {
|
||||
chatLog = {
|
||||
...chatLog,
|
||||
content: [...chatLog.content, processContent(event.data.content)],
|
||||
};
|
||||
callback(chatLog);
|
||||
}
|
||||
} else if (event.event_type === ChatLogEventType.UPDATED) {
|
||||
chatLog = processChatLog(event.data.chat_log);
|
||||
callback(chatLog);
|
||||
} else if (event.event_type === ChatLogEventType.DELETED) {
|
||||
chatLog = null;
|
||||
callback(null);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "conversation/chat_log/subscribe",
|
||||
conversation_id: conversationId,
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const subscribeChatLogIndex = (
|
||||
hass: HomeAssistant,
|
||||
callback: (chatLogs: ChatLog[]) => void
|
||||
): Promise<UnsubscribeFunc> => {
|
||||
let chatLogs: ChatLog[] = [];
|
||||
|
||||
return hass.connection.subscribeMessage<ChatLogIndexSubscriptionEvent>(
|
||||
(event) => {
|
||||
if (event.event_type === ChatLogEventType.INITIAL_STATE) {
|
||||
chatLogs = event.data.map(processChatLog);
|
||||
callback(chatLogs);
|
||||
} else if (event.event_type === ChatLogEventType.CREATED) {
|
||||
chatLogs = [...chatLogs, processChatLog(event.data)];
|
||||
callback(chatLogs);
|
||||
} else if (event.event_type === ChatLogEventType.DELETED) {
|
||||
chatLogs = chatLogs.filter(
|
||||
(chatLog) => chatLog.conversation_id !== event.conversation_id
|
||||
);
|
||||
callback(chatLogs);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "conversation/chat_log/subscribe_index",
|
||||
}
|
||||
);
|
||||
};
|
||||
@@ -1,38 +1,15 @@
|
||||
import {
|
||||
mdiAmpersand,
|
||||
mdiClockOutline,
|
||||
mdiCodeBraces,
|
||||
mdiDevices,
|
||||
mdiGateOr,
|
||||
mdiIdentifier,
|
||||
mdiMapClock,
|
||||
mdiMapMarkerRadius,
|
||||
mdiNotEqualVariant,
|
||||
mdiNumeric,
|
||||
mdiShape,
|
||||
mdiStateMachine,
|
||||
mdiWeatherSunny,
|
||||
} from "@mdi/js";
|
||||
import { mdiMapClock, mdiShape } from "@mdi/js";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../common/entity/compute_object_id";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { AutomationElementGroupCollection } from "./automation";
|
||||
|
||||
export const CONDITION_ICONS = {
|
||||
device: mdiDevices,
|
||||
and: mdiAmpersand,
|
||||
or: mdiGateOr,
|
||||
not: mdiNotEqualVariant,
|
||||
state: mdiStateMachine,
|
||||
numeric_state: mdiNumeric,
|
||||
sun: mdiWeatherSunny,
|
||||
template: mdiCodeBraces,
|
||||
time: mdiClockOutline,
|
||||
trigger: mdiIdentifier,
|
||||
zone: mdiMapMarkerRadius,
|
||||
};
|
||||
import type { Selector, TargetSelector } from "./selector";
|
||||
|
||||
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
|
||||
{
|
||||
groups: {
|
||||
device: {},
|
||||
dynamicGroups: {},
|
||||
entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
|
||||
time_location: {
|
||||
icon: mdiMapClock,
|
||||
@@ -62,3 +39,33 @@ export const COLLAPSIBLE_CONDITION_ELEMENTS = [
|
||||
"ha-automation-condition-not",
|
||||
"ha-automation-condition-or",
|
||||
];
|
||||
|
||||
export interface ConditionDescription {
|
||||
target?: TargetSelector["target"];
|
||||
fields: Record<
|
||||
string,
|
||||
{
|
||||
example?: string | boolean | number;
|
||||
default?: unknown;
|
||||
required?: boolean;
|
||||
selector?: Selector;
|
||||
context?: Record<string, string>;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
export type ConditionDescriptions = Record<string, ConditionDescription>;
|
||||
|
||||
export const subscribeConditions = (
|
||||
hass: HomeAssistant,
|
||||
callback: (conditions: ConditionDescriptions) => void
|
||||
) =>
|
||||
hass.connection.subscribeMessage<ConditionDescriptions>(callback, {
|
||||
type: "condition_platforms/subscribe",
|
||||
});
|
||||
|
||||
export const getConditionDomain = (condition: string) =>
|
||||
condition.includes(".") ? computeDomain(condition) : condition;
|
||||
|
||||
export const getConditionObjectId = (condition: string) =>
|
||||
condition.includes(".") ? computeObjectId(condition) : "_";
|
||||
|
||||
@@ -50,7 +50,11 @@ export type DeviceEntityDisplayLookup = Record<
|
||||
EntityRegistryDisplayEntry[]
|
||||
>;
|
||||
|
||||
export type DeviceEntityLookup = Record<string, EntityRegistryEntry[]>;
|
||||
export type DeviceEntityLookup<
|
||||
T extends EntityRegistryEntry | EntityRegistryDisplayEntry =
|
||||
| EntityRegistryEntry
|
||||
| EntityRegistryDisplayEntry,
|
||||
> = Record<string, T[]>;
|
||||
|
||||
export interface DeviceRegistryEntryMutableParams {
|
||||
area_id?: string | null;
|
||||
@@ -107,11 +111,17 @@ export const sortDeviceRegistryByName = (
|
||||
);
|
||||
|
||||
export const getDeviceEntityLookup = (
|
||||
entities: EntityRegistryEntry[]
|
||||
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
|
||||
filterHidden = false
|
||||
): DeviceEntityLookup => {
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
if (
|
||||
!entity.device_id ||
|
||||
(filterHidden &&
|
||||
((entity as EntityRegistryDisplayEntry).hidden ||
|
||||
(entity as EntityRegistryEntry).hidden_by))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!(entity.device_id in deviceEntityLookup)) {
|
||||
|
||||
@@ -775,6 +775,7 @@ export const getEnergyDataCollection = (
|
||||
hass.locale,
|
||||
hass.config
|
||||
);
|
||||
collection.refresh();
|
||||
scheduleUpdatePeriod();
|
||||
},
|
||||
addHours(
|
||||
|
||||
@@ -7,8 +7,8 @@ export interface CoreFrontendUserData {
|
||||
}
|
||||
|
||||
export interface SidebarFrontendUserData {
|
||||
panelOrder: string[];
|
||||
hiddenPanels: string[];
|
||||
panelOrder?: string[];
|
||||
hiddenPanels?: string[];
|
||||
}
|
||||
|
||||
export interface CoreFrontendSystemData {
|
||||
|
||||
@@ -60,6 +60,7 @@ import type {
|
||||
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
|
||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||
|
||||
/** Icon to use when no icon specified for service. */
|
||||
export const DEFAULT_SERVICE_ICON = mdiRoomService;
|
||||
@@ -138,15 +139,25 @@ const resources: {
|
||||
all?: Promise<Record<string, TriggerIcons>>;
|
||||
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
|
||||
};
|
||||
conditions: {
|
||||
all?: Promise<Record<string, ConditionIcons>>;
|
||||
domains: Record<string, ConditionIcons | Promise<ConditionIcons>>;
|
||||
};
|
||||
} = {
|
||||
entity: {},
|
||||
entity_component: {},
|
||||
services: { domains: {} },
|
||||
triggers: { domains: {} },
|
||||
conditions: { domains: {} },
|
||||
};
|
||||
|
||||
interface IconResources<
|
||||
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
|
||||
T extends
|
||||
| ComponentIcons
|
||||
| PlatformIcons
|
||||
| ServiceIcons
|
||||
| TriggerIcons
|
||||
| ConditionIcons,
|
||||
> {
|
||||
resources: Record<string, T>;
|
||||
}
|
||||
@@ -195,17 +206,24 @@ type TriggerIcons = Record<
|
||||
{ trigger: string; sections?: Record<string, string> }
|
||||
>;
|
||||
|
||||
type ConditionIcons = Record<
|
||||
string,
|
||||
{ condition: string; sections?: Record<string, string> }
|
||||
>;
|
||||
|
||||
export type IconCategory =
|
||||
| "entity"
|
||||
| "entity_component"
|
||||
| "services"
|
||||
| "triggers";
|
||||
| "triggers"
|
||||
| "conditions";
|
||||
|
||||
interface CategoryType {
|
||||
entity: PlatformIcons;
|
||||
entity_component: ComponentIcons;
|
||||
services: ServiceIcons;
|
||||
triggers: TriggerIcons;
|
||||
conditions: ConditionIcons;
|
||||
}
|
||||
|
||||
export const getHassIcons = async <T extends IconCategory>(
|
||||
@@ -327,6 +345,13 @@ export const getTriggerIcons = async (
|
||||
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> =>
|
||||
getCategoryIcons(hass, "triggers", domain, force);
|
||||
|
||||
export const getConditionIcons = async (
|
||||
hass: HomeAssistant,
|
||||
domain?: string,
|
||||
force = false
|
||||
): Promise<ConditionIcons | Record<string, ConditionIcons> | undefined> =>
|
||||
getCategoryIcons(hass, "conditions", domain, force);
|
||||
|
||||
// Cache for sorted range keys
|
||||
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
|
||||
|
||||
@@ -526,6 +551,25 @@ export const triggerIcon = async (
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const conditionIcon = async (
|
||||
hass: HomeAssistant,
|
||||
condition: string
|
||||
): Promise<string | undefined> => {
|
||||
let icon: string | undefined;
|
||||
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionIcons = await getConditionIcons(hass, domain);
|
||||
if (conditionIcons) {
|
||||
const conditionName = getConditionObjectId(condition);
|
||||
const condIcon = conditionIcons[conditionName] as ConditionIcons[string];
|
||||
icon = condIcon?.condition;
|
||||
}
|
||||
if (!icon) {
|
||||
icon = await domainIcon(hass, domain);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const serviceIcon = async (
|
||||
hass: HomeAssistant,
|
||||
service: string
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { createCollection } from "home-assistant-js-websocket";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import { debounce } from "../common/util/debounce";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export const integrationsWithPanel = {
|
||||
bluetooth: "config/bluetooth",
|
||||
@@ -25,6 +25,8 @@ export type IntegrationType =
|
||||
| "entity"
|
||||
| "system";
|
||||
|
||||
export type DomainManifestLookup = Record<string, IntegrationManifest>;
|
||||
|
||||
export interface IntegrationManifest {
|
||||
is_built_in: boolean;
|
||||
overwrites_built_in?: boolean;
|
||||
|
||||
@@ -101,7 +101,10 @@ export const deleteLabelRegistryEntry = (
|
||||
});
|
||||
|
||||
export const getLabels = (
|
||||
hass: HomeAssistant,
|
||||
hassStates: HomeAssistant["states"],
|
||||
hassAreas: HomeAssistant["areas"],
|
||||
hassDevices: HomeAssistant["devices"],
|
||||
hassEntities: HomeAssistant["entities"],
|
||||
labels?: LabelRegistryEntry[],
|
||||
includeDomains?: string[],
|
||||
excludeDomains?: string[],
|
||||
@@ -115,8 +118,8 @@ export const getLabels = (
|
||||
return [];
|
||||
}
|
||||
|
||||
const devices = Object.values(hass.devices);
|
||||
const entities = Object.values(hass.entities);
|
||||
const devices = Object.values(hassDevices);
|
||||
const entities = Object.values(hassEntities);
|
||||
|
||||
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
|
||||
let inputDevices: DeviceRegistryEntry[] | undefined;
|
||||
@@ -170,7 +173,7 @@ export const getLabels = (
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -181,8 +184,9 @@ export const getLabels = (
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
return (
|
||||
stateObj &&
|
||||
stateObj.attributes.device_class &&
|
||||
includeDeviceClasses.includes(stateObj.attributes.device_class)
|
||||
);
|
||||
@@ -200,7 +204,7 @@ export const getLabels = (
|
||||
return false;
|
||||
}
|
||||
return deviceEntityLookup[device.id].some((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -208,7 +212,7 @@ export const getLabels = (
|
||||
});
|
||||
});
|
||||
inputEntities = inputEntities!.filter((entity) => {
|
||||
const stateObj = hass.states[entity.entity_id];
|
||||
const stateObj = hassStates[entity.entity_id];
|
||||
if (!stateObj) {
|
||||
return false;
|
||||
}
|
||||
@@ -245,8 +249,8 @@ export const getLabels = (
|
||||
|
||||
if (areaIds) {
|
||||
areaIds.forEach((areaId) => {
|
||||
const area = hass.areas[areaId];
|
||||
area.labels.forEach((label) => usedLabels.add(label));
|
||||
const area = hassAreas[areaId];
|
||||
area?.labels.forEach((label) => usedLabels.add(label));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiCalendar,
|
||||
mdiChartBox,
|
||||
mdiClipboardList,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiLightningBolt,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiTooltipAccount,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
@@ -60,7 +72,7 @@ export const getPanelTitleFromUrlPath = (
|
||||
return getPanelTitle(hass, panel);
|
||||
};
|
||||
|
||||
export const getPanelIcon = (panel: PanelInfo): string | null => {
|
||||
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
|
||||
if (!panel.icon) {
|
||||
switch (panel.component_name) {
|
||||
case "profile":
|
||||
@@ -70,5 +82,24 @@ export const getPanelIcon = (panel: PanelInfo): string | null => {
|
||||
}
|
||||
}
|
||||
|
||||
return panel.icon;
|
||||
return panel.icon || undefined;
|
||||
};
|
||||
|
||||
export const PANEL_ICON_PATHS = {
|
||||
calendar: mdiCalendar,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
lovelace: mdiViewDashboard,
|
||||
profile: mdiAccount,
|
||||
map: mdiTooltipAccount,
|
||||
"media-browser": mdiPlayBoxMultiple,
|
||||
todo: mdiClipboardList,
|
||||
};
|
||||
|
||||
export const getPanelIconPath = (panel: PanelInfo): string | undefined =>
|
||||
PANEL_ICON_PATHS[panel.url_path];
|
||||
|
||||
export const FIXED_PANELS = ["profile", "config"];
|
||||
export const SHOW_AFTER_SPACER_PANELS = ["developer-tools"];
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
|
||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { FloorComboBoxItem } from "./area_floor";
|
||||
import type { AreaRegistryEntry } from "./area_registry";
|
||||
import type { DeviceRegistryEntry } from "./device_registry";
|
||||
import type { DevicePickerItem, DeviceRegistryEntry } from "./device_registry";
|
||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
|
||||
import type { EntityRegistryDisplayEntry } from "./entity_registry";
|
||||
import type {
|
||||
EntityComboBoxItem,
|
||||
EntityRegistryDisplayEntry,
|
||||
} from "./entity_registry";
|
||||
|
||||
export const TARGET_SEPARATOR = "________";
|
||||
|
||||
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
|
||||
export type TargetTypeFloorless = Exclude<TargetType, "floor">;
|
||||
|
||||
export interface SingleHassServiceTarget {
|
||||
entity_id?: string;
|
||||
device_id?: string;
|
||||
area_id?: string;
|
||||
floor_id?: string;
|
||||
label_id?: string;
|
||||
}
|
||||
|
||||
export interface ExtractFromTargetResult {
|
||||
missing_areas: string[];
|
||||
missing_devices: string[];
|
||||
@@ -35,6 +50,39 @@ export const extractFromTarget = async (
|
||||
target,
|
||||
});
|
||||
|
||||
export const getTriggersForTarget = async (
|
||||
callWS: HomeAssistant["callWS"],
|
||||
target: HassServiceTarget,
|
||||
expandGroup = true
|
||||
) =>
|
||||
callWS<string[]>({
|
||||
type: "get_triggers_for_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
});
|
||||
|
||||
export const getConditionsForTarget = async (
|
||||
callWS: HomeAssistant["callWS"],
|
||||
target: HassServiceTarget,
|
||||
expandGroup = true
|
||||
) =>
|
||||
callWS<string[]>({
|
||||
type: "get_conditions_for_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
});
|
||||
|
||||
export const getServicesForTarget = async (
|
||||
callWS: HomeAssistant["callWS"],
|
||||
target: HassServiceTarget,
|
||||
expandGroup = true
|
||||
) =>
|
||||
callWS<string[]>({
|
||||
type: "get_services_for_target",
|
||||
target,
|
||||
expand_group: expandGroup,
|
||||
});
|
||||
|
||||
export const areaMeetsFilter = (
|
||||
area: AreaRegistryEntry,
|
||||
devices: HomeAssistant["devices"],
|
||||
@@ -162,3 +210,32 @@ export const entityRegMeetsFilter = (
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export const getTargetComboBoxItemType = (
|
||||
item:
|
||||
| PickerComboBoxItem
|
||||
| (FloorComboBoxItem & { last?: boolean | undefined })
|
||||
| EntityComboBoxItem
|
||||
| DevicePickerItem
|
||||
) => {
|
||||
if (
|
||||
(item as FloorComboBoxItem).type === "area" ||
|
||||
(item as FloorComboBoxItem).type === "floor"
|
||||
) {
|
||||
return (item as FloorComboBoxItem).type;
|
||||
}
|
||||
|
||||
if ("domain" in item) {
|
||||
return "device";
|
||||
}
|
||||
|
||||
if ("stateObj" in item) {
|
||||
return "entity";
|
||||
}
|
||||
|
||||
if (item.id === "___EMPTY_SEARCH___") {
|
||||
return "empty";
|
||||
}
|
||||
|
||||
return "label";
|
||||
};
|
||||
|
||||
@@ -75,7 +75,8 @@ export type TranslationCategory =
|
||||
| "preview_features"
|
||||
| "selector"
|
||||
| "services"
|
||||
| "triggers";
|
||||
| "triggers"
|
||||
| "conditions";
|
||||
|
||||
export const subscribeTranslationPreferences = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -2,16 +2,16 @@ import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-icon";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-md-list-item";
|
||||
import "../../components/ha-spinner";
|
||||
import type {
|
||||
ExternalEntityAddToActions,
|
||||
ExternalEntityAddToAction,
|
||||
ExternalEntityAddToActions,
|
||||
} from "../../external_app/external_messaging";
|
||||
import { showToast } from "../../util/toast";
|
||||
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-more-info-add-to")
|
||||
export class HaMoreInfoAddTo extends LitElement {
|
||||
@@ -93,19 +93,18 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
<div class="actions-list">
|
||||
${this._externalActions.actions.map(
|
||||
(action) => html`
|
||||
<ha-list-item
|
||||
graphic="icon"
|
||||
<ha-md-list-item
|
||||
type="button"
|
||||
.disabled=${!action.enabled}
|
||||
.action=${action}
|
||||
.twoline=${!!action.details}
|
||||
@click=${this._actionSelected}
|
||||
>
|
||||
<ha-icon slot="start" .icon=${action.mdi_icon}></ha-icon>
|
||||
<span>${action.name}</span>
|
||||
${action.details
|
||||
? html`<span slot="secondary">${action.details}</span>`
|
||||
? html`<span slot="supporting-text">${action.details}</span>`
|
||||
: nothing}
|
||||
<ha-icon slot="graphic" .icon=${action.mdi_icon}></ha-icon>
|
||||
</ha-list-item>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -131,15 +130,6 @@ export class HaMoreInfoAddTo extends LitElement {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ha-list-item {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ha-list-item[disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
ha-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { mdiClose, mdiDotsVertical, mdiRestart } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -9,18 +9,30 @@ import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-fade-in";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-items-display-editor";
|
||||
import type { DisplayValue } from "../../components/ha-items-display-editor";
|
||||
import type {
|
||||
DisplayItem,
|
||||
DisplayValue,
|
||||
} from "../../components/ha-items-display-editor";
|
||||
import "../../components/ha-md-button-menu";
|
||||
import "../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../components/ha-md-dialog";
|
||||
import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar";
|
||||
import "../../components/ha-md-menu-item";
|
||||
import { computePanels } from "../../components/ha-sidebar";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-svg-icon";
|
||||
import {
|
||||
fetchFrontendUserData,
|
||||
saveFrontendUserData,
|
||||
} from "../../data/frontend";
|
||||
import {
|
||||
getDefaultPanelUrlPath,
|
||||
getPanelIcon,
|
||||
getPanelIconPath,
|
||||
getPanelTitle,
|
||||
SHOW_AFTER_SPACER_PANELS,
|
||||
} from "../../data/panel";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { getDefaultPanelUrlPath } from "../../data/panel";
|
||||
|
||||
@customElement("dialog-edit-sidebar")
|
||||
class DialogEditSidebar extends LitElement {
|
||||
@@ -105,48 +117,53 @@ class DialogEditSidebar extends LitElement {
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
// Add default hidden panels that are missing in hidden
|
||||
const orderSet = new Set(this._order);
|
||||
const hiddenSet = new Set(this._hidden);
|
||||
|
||||
for (const panel of panels) {
|
||||
if (
|
||||
panel.default_visible === false &&
|
||||
!this._order.includes(panel.url_path) &&
|
||||
!this._hidden.includes(panel.url_path)
|
||||
!orderSet.has(panel.url_path) &&
|
||||
!hiddenSet.has(panel.url_path)
|
||||
) {
|
||||
this._hidden.push(panel.url_path);
|
||||
hiddenSet.add(panel.url_path);
|
||||
}
|
||||
}
|
||||
|
||||
if (hiddenSet.has(defaultPanel)) {
|
||||
hiddenSet.delete(defaultPanel);
|
||||
}
|
||||
|
||||
const hiddenPanels = Array.from(hiddenSet);
|
||||
|
||||
const items = [
|
||||
...beforeSpacer,
|
||||
...panels.filter((panel) => this._hidden!.includes(panel.url_path)),
|
||||
...afterSpacer.filter((panel) => panel.url_path !== "config"),
|
||||
].map((panel) => ({
|
||||
...panels.filter((panel) => hiddenPanels.includes(panel.url_path)),
|
||||
...afterSpacer,
|
||||
].map<DisplayItem>((panel) => ({
|
||||
value: panel.url_path,
|
||||
label:
|
||||
panel.url_path === defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
|
||||
icon: panel.icon || undefined,
|
||||
iconPath:
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
: undefined,
|
||||
disableSorting: panel.url_path === "developer-tools",
|
||||
(getPanelTitle(this.hass, panel) || panel.url_path) +
|
||||
`${defaultPanel === panel.url_path ? " (default)" : ""}`,
|
||||
icon: getPanelIcon(panel),
|
||||
iconPath: getPanelIconPath(panel),
|
||||
disableSorting: SHOW_AFTER_SPACER_PANELS.includes(panel.url_path),
|
||||
disableHiding: panel.url_path === defaultPanel,
|
||||
}));
|
||||
|
||||
return html`<ha-items-display-editor
|
||||
return html`
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.value=${{
|
||||
order: this._order,
|
||||
hidden: this._hidden,
|
||||
hidden: hiddenPanels,
|
||||
}}
|
||||
.items=${items}
|
||||
@value-changed=${this._changed}
|
||||
dont-sort-visible
|
||||
>
|
||||
</ha-items-display-editor>`;
|
||||
</ha-items-display-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -171,6 +188,22 @@ class DialogEditSidebar extends LitElement {
|
||||
>${this.hass.localize("ui.sidebar.edit_subtitle")}</span
|
||||
>`
|
||||
: nothing}
|
||||
<ha-md-button-menu
|
||||
slot="actionItems"
|
||||
positioning="popover"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item .clickAction=${this._resetToDefaults}>
|
||||
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
|
||||
${this.hass.localize("ui.sidebar.reset_to_defaults")}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-dialog-header>
|
||||
<div slot="content" class="content">${this._renderContent()}</div>
|
||||
<div slot="actions">
|
||||
@@ -194,6 +227,26 @@ class DialogEditSidebar extends LitElement {
|
||||
this._hidden = [...hidden];
|
||||
}
|
||||
|
||||
private _resetToDefaults = async () => {
|
||||
const confirmation = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize("ui.sidebar.reset_confirmation"),
|
||||
confirmText: this.hass.localize("ui.common.reset"),
|
||||
});
|
||||
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._order = [];
|
||||
this._hidden = [];
|
||||
try {
|
||||
await saveFrontendUserData(this.hass.connection, "sidebar", {});
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
}
|
||||
this.closeDialog();
|
||||
};
|
||||
|
||||
private async _save() {
|
||||
if (this._migrateToUserData) {
|
||||
const confirmation = await showConfirmationDialog(this, {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { listenMediaQuery } from "../common/dom/media_query";
|
||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
|
||||
import { computeRTLDirection } from "../common/util/compute_rtl";
|
||||
import "../components/ha-drawer";
|
||||
import "../components/ha-snowflakes";
|
||||
import { showNotificationDrawer } from "../dialogs/notifications/show-notification-drawer";
|
||||
import type { HomeAssistant, Route } from "../types";
|
||||
import "./partial-panel-resolver";
|
||||
@@ -50,6 +51,7 @@ export class HomeAssistantMain extends LitElement {
|
||||
this.hass.panels && this.hass.userData && this.hass.systemData;
|
||||
|
||||
return html`
|
||||
<ha-snowflakes .hass=${this.hass} .narrow=${this.narrow}></ha-snowflakes>
|
||||
<ha-drawer
|
||||
.type=${sidebarNarrow ? "modal" : ""}
|
||||
.open=${sidebarNarrow ? this._drawerOpen : false}
|
||||
|
||||
187
src/mixins/scrollable-fade-mixin.ts
Normal file
187
src/mixins/scrollable-fade-mixin.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { css, html } from "lit";
|
||||
import type {
|
||||
CSSResultGroup,
|
||||
LitElement,
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { state } from "lit/decorators";
|
||||
import type { Constructor } from "../types";
|
||||
|
||||
const stylesArray = (styles?: CSSResultGroup | CSSResultGroup[]) =>
|
||||
styles === undefined ? [] : Array.isArray(styles) ? styles : [styles];
|
||||
|
||||
export const ScrollableFadeMixin = <T extends Constructor<LitElement>>(
|
||||
superClass: T
|
||||
) => {
|
||||
class ScrollableFadeClass extends superClass {
|
||||
@state() protected _contentScrolled = false;
|
||||
|
||||
@state() protected _contentScrollable = false;
|
||||
|
||||
private _scrollTarget?: HTMLElement | null;
|
||||
|
||||
private _onScroll = (ev: Event) => {
|
||||
const target = ev.currentTarget as HTMLElement;
|
||||
this._contentScrolled = (target.scrollTop ?? 0) > 0;
|
||||
this._updateScrollableState(target);
|
||||
};
|
||||
|
||||
private _resize = new ResizeController(this, {
|
||||
target: null,
|
||||
callback: (entries) => {
|
||||
const target = entries[0]?.target as HTMLElement | undefined;
|
||||
if (target) {
|
||||
this._updateScrollableState(target);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
private static readonly DEFAULT_SAFE_AREA_PADDING = 16;
|
||||
|
||||
private static readonly DEFAULT_SCROLLABLE_ELEMENT: HTMLElement | null =
|
||||
null;
|
||||
|
||||
protected get scrollFadeSafeAreaPadding() {
|
||||
return ScrollableFadeClass.DEFAULT_SAFE_AREA_PADDING;
|
||||
}
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return ScrollableFadeClass.DEFAULT_SCROLLABLE_ELEMENT;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProperties: PropertyValues) {
|
||||
super.firstUpdated?.(changedProperties);
|
||||
this._attachScrollableElement();
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
super.updated?.(changedProperties);
|
||||
this._attachScrollableElement();
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
this._detachScrollableElement();
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
protected renderScrollableFades(rounded = false): TemplateResult {
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
"fade-top": true,
|
||||
rounded,
|
||||
visible: this._contentScrolled,
|
||||
})}
|
||||
></div>
|
||||
<div
|
||||
class=${classMap({
|
||||
"fade-bottom": true,
|
||||
rounded,
|
||||
visible: this._contentScrollable,
|
||||
})}
|
||||
></div>
|
||||
`;
|
||||
}
|
||||
|
||||
static get styles() {
|
||||
const superCtor = Object.getPrototypeOf(this) as
|
||||
| typeof LitElement
|
||||
| undefined;
|
||||
const inheritedStyles = stylesArray(
|
||||
(superCtor?.styles ?? []) as CSSResultGroup | CSSResultGroup[]
|
||||
);
|
||||
return [
|
||||
...inheritedStyles,
|
||||
css`
|
||||
.fade-top,
|
||||
.fade-bottom {
|
||||
position: absolute;
|
||||
left: var(--ha-space-0);
|
||||
right: var(--ha-space-0);
|
||||
height: var(--ha-space-4);
|
||||
pointer-events: none;
|
||||
transition: opacity 180ms ease-in-out;
|
||||
background: linear-gradient(
|
||||
to bottom,
|
||||
var(--shadow-color),
|
||||
transparent
|
||||
);
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
}
|
||||
.fade-top {
|
||||
top: var(--ha-space-0);
|
||||
}
|
||||
.fade-bottom {
|
||||
bottom: var(--ha-space-0);
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.fade-top.visible,
|
||||
.fade-bottom.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.fade-top.rounded,
|
||||
.fade-bottom.rounded {
|
||||
border-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
.fade-top.rounded {
|
||||
border-top-left-radius: var(--ha-border-radius-square);
|
||||
border-top-right-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
.fade-bottom.rounded {
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
private _attachScrollableElement() {
|
||||
const element = this.scrollableElement;
|
||||
if (element === this._scrollTarget) {
|
||||
return;
|
||||
}
|
||||
this._detachScrollableElement();
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
this._scrollTarget = element;
|
||||
element.addEventListener("scroll", this._onScroll, { passive: true });
|
||||
this._resize.observe(element);
|
||||
this._updateScrollableState(element);
|
||||
}
|
||||
|
||||
private _detachScrollableElement() {
|
||||
if (!this._scrollTarget) {
|
||||
return;
|
||||
}
|
||||
this._scrollTarget.removeEventListener("scroll", this._onScroll);
|
||||
this._resize.unobserve?.(this._scrollTarget);
|
||||
this._scrollTarget = undefined;
|
||||
}
|
||||
|
||||
private _updateScrollableState(element: HTMLElement) {
|
||||
const safeAreaInsetBottom =
|
||||
parseFloat(
|
||||
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
|
||||
) || 0;
|
||||
const { scrollHeight = 0, clientHeight = 0, scrollTop = 0 } = element;
|
||||
this._contentScrollable =
|
||||
scrollHeight - clientHeight >
|
||||
scrollTop + safeAreaInsetBottom + this.scrollFadeSafeAreaPadding;
|
||||
}
|
||||
}
|
||||
|
||||
return ScrollableFadeClass;
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
@@ -213,7 +214,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _addAction = (action: string) => {
|
||||
private _addAction = (action: string, target?: HassServiceTarget) => {
|
||||
let actions: Action[];
|
||||
if (action === PASTE_VALUE) {
|
||||
actions = this.actions.concat(deepClone(this._clipboard!.action));
|
||||
@@ -223,6 +224,7 @@ export default class HaAutomationAction extends LitElement {
|
||||
actions = this.actions.concat({
|
||||
action: getValueFromDynamic(action),
|
||||
metadata: {},
|
||||
target,
|
||||
});
|
||||
} else {
|
||||
const elClass = customElements.get(
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../../common/string/compare";
|
||||
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../../../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../../components/ha-select";
|
||||
import type { Condition } from "../../../../../data/automation";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type Condition,
|
||||
} from "../../../../../data/automation";
|
||||
import type { ConditionDescriptions } from "../../../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
CONDITION_ICONS,
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
subscribeConditions,
|
||||
} from "../../../../../data/condition";
|
||||
import type { Entries, HomeAssistant } from "../../../../../types";
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../../condition/ha-automation-condition-editor";
|
||||
import "../../condition/types/ha-automation-condition-and";
|
||||
@@ -30,7 +40,10 @@ import "../../condition/types/ha-automation-condition-zone";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-condition")
|
||||
export class HaConditionAction extends LitElement implements ActionElement {
|
||||
export class HaConditionAction
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements ActionElement
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -43,6 +56,8 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "indent" }) public indent = false;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@query("ha-automation-condition-editor")
|
||||
private _conditionEditor?: HaAutomationConditionEditor;
|
||||
|
||||
@@ -50,6 +65,21 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
return { condition: "state" };
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeConditions(this.hass, (conditions) =>
|
||||
this._addConditions(conditions)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _addConditions(conditions: ConditionDescriptions) {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
|
||||
this.action.condition
|
||||
@@ -64,19 +94,25 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
"ui.panel.config.automation.editor.conditions.type_select"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.action.condition}
|
||||
.value=${this.action.condition in this._conditionDescriptions
|
||||
? `${DYNAMIC_PREFIX}${this.action.condition}`
|
||||
: this.action.condition}
|
||||
naturalMenuWidth
|
||||
@selected=${this._typeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._processedTypes(this.hass.localize).map(
|
||||
([opt, label, icon]) => html`
|
||||
${this._processedTypes(
|
||||
this._conditionDescriptions,
|
||||
this.hass.localize
|
||||
).map(
|
||||
([opt, label, condition]) => html`
|
||||
<ha-list-item .value=${opt} graphic="icon">
|
||||
${label}<ha-svg-icon
|
||||
${label}
|
||||
<ha-condition-icon
|
||||
slot="graphic"
|
||||
.path=${icon}
|
||||
></ha-svg-icon
|
||||
></ha-list-item>
|
||||
.condition=${condition}
|
||||
></ha-condition-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
@@ -88,11 +124,14 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
? html`
|
||||
<ha-automation-condition-editor
|
||||
.condition=${this.action}
|
||||
.description=${this._conditionDescriptions[this.action.condition]}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.narrow=${this.narrow}
|
||||
.uiSupported=${this._uiSupported(this.action.condition)}
|
||||
.uiSupported=${this._uiSupported(
|
||||
this._getType(this.action, this._conditionDescriptions)
|
||||
)}
|
||||
.indent=${this.indent}
|
||||
action
|
||||
></ha-automation-condition-editor>
|
||||
@@ -102,19 +141,46 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
private _processedTypes = memoizeOne(
|
||||
(localize: LocalizeFunc): [string, string, string][] =>
|
||||
(Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
|
||||
.map(
|
||||
([condition, icon]) =>
|
||||
(
|
||||
conditionDescriptions: ConditionDescriptions,
|
||||
localize: LocalizeFunc
|
||||
): [string, string, string][] => {
|
||||
const legacy = (
|
||||
Object.keys(CONDITION_ICONS) as (keyof typeof CONDITION_ICONS)[]
|
||||
).map(
|
||||
(condition) =>
|
||||
[
|
||||
condition,
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
|
||||
),
|
||||
icon,
|
||||
condition,
|
||||
] as [string, string, string]
|
||||
)
|
||||
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
|
||||
);
|
||||
const platform = Object.keys(conditionDescriptions).map((condition) => {
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionObjId = getConditionObjectId(condition);
|
||||
return [
|
||||
`${DYNAMIC_PREFIX}${condition}`,
|
||||
localize(`component.${domain}.conditions.${conditionObjId}.name`) ||
|
||||
condition,
|
||||
condition,
|
||||
] as [string, string, string];
|
||||
});
|
||||
return [...legacy, ...platform].sort((a, b) =>
|
||||
stringCompare(a[1], b[1], this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _getType = memoizeOne(
|
||||
(condition: Condition, conditionDescriptions: ConditionDescriptions) => {
|
||||
if (condition.condition in conditionDescriptions) {
|
||||
return "platform";
|
||||
}
|
||||
|
||||
return condition.condition;
|
||||
}
|
||||
);
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
@@ -132,6 +198,18 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDynamic(type)) {
|
||||
const value = getValueFromDynamic(type);
|
||||
if (value !== this.action.condition) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
condition: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const elClass = customElements.get(
|
||||
`ha-automation-condition-${type}`
|
||||
) as CustomElementConstructor & {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,384 @@
|
||||
import {
|
||||
mdiInformationOutline,
|
||||
mdiLabel,
|
||||
mdiPlus,
|
||||
mdiTextureBox,
|
||||
} from "@mdi/js";
|
||||
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
import "../../../../components/entity/state-badge";
|
||||
import "../../../../components/ha-domain-icon";
|
||||
import "../../../../components/ha-floor-icon";
|
||||
import "../../../../components/ha-icon-next";
|
||||
import "../../../../components/ha-md-list";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import type { ConfigEntry } from "../../../../data/config_entries";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
|
||||
|
||||
type Target = [string, string | undefined, string | undefined];
|
||||
|
||||
@customElement("ha-automation-add-items")
|
||||
export class HaAutomationAddItems extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public items?: {
|
||||
title: string;
|
||||
items: AddAutomationElementListItem[];
|
||||
}[];
|
||||
|
||||
@property() public error?: string;
|
||||
|
||||
@property({ attribute: "select-label" }) public selectLabel!: string;
|
||||
|
||||
@property({ attribute: "empty-label" }) public emptyLabel!: string;
|
||||
|
||||
@property({ attribute: false }) public target?: Target;
|
||||
|
||||
@property({ attribute: false }) public getLabel!: (
|
||||
id: string
|
||||
) => { name: string; icon?: string } | undefined;
|
||||
|
||||
@property({ attribute: false }) public configEntryLookup: Record<
|
||||
string,
|
||||
ConfigEntry
|
||||
> = {};
|
||||
|
||||
@property({ type: Boolean, attribute: "tooltip-description" })
|
||||
public tooltipDescription = false;
|
||||
|
||||
@state() private _itemsScrolled = false;
|
||||
|
||||
@query(".items")
|
||||
private _itemsDiv!: HTMLDivElement;
|
||||
|
||||
protected render() {
|
||||
return html`<div
|
||||
class=${classMap({
|
||||
items: true,
|
||||
blank: this.error || !this.items || !this.items.length,
|
||||
error: this.error,
|
||||
scrolled: this._itemsScrolled,
|
||||
})}
|
||||
@scroll=${this._onItemsScroll}
|
||||
>
|
||||
${!this.items && !this.error
|
||||
? this.selectLabel
|
||||
: this.error
|
||||
? html`${this.error}
|
||||
<div>${this._renderTarget(this.target)}</div>`
|
||||
: this.items && !this.items.length
|
||||
? html`${this.emptyLabel}
|
||||
${this.target
|
||||
? html`<div>${this._renderTarget(this.target)}</div>`
|
||||
: nothing}`
|
||||
: repeat(
|
||||
this.items,
|
||||
(_, index) => `item-group-${index}`,
|
||||
(itemGroup) =>
|
||||
this._renderItemList(itemGroup.title, itemGroup.items)
|
||||
)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderItemList(title, items?: AddAutomationElementListItem[]) {
|
||||
if (!items || !items.length) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`
|
||||
<div class="items-title">${title}</div>
|
||||
<ha-md-list>
|
||||
${repeat(
|
||||
items,
|
||||
(item) => item.key,
|
||||
(item) => html`
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
.value=${item.key}
|
||||
@click=${this._selected}
|
||||
>
|
||||
<div slot="headline" class=${this.target ? "item-headline" : ""}>
|
||||
${item.name}${this._renderTarget(this.target)}
|
||||
</div>
|
||||
|
||||
${!this.tooltipDescription && item.description
|
||||
? html`<div slot="supporting-text">${item.description}</div>`
|
||||
: nothing}
|
||||
${item.icon
|
||||
? html`<span slot="start">${item.icon}</span>`
|
||||
: item.iconPath
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.iconPath}
|
||||
></ha-svg-icon>`
|
||||
: nothing}
|
||||
${this.tooltipDescription && item.description
|
||||
? html`<ha-svg-icon
|
||||
tabindex="0"
|
||||
id=${`description-tooltip-${item.key}`}
|
||||
slot="end"
|
||||
.path=${mdiInformationOutline}
|
||||
@click=${stopPropagation}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
.for=${`description-tooltip-${item.key}`}
|
||||
@wa-show=${stopPropagation}
|
||||
@wa-hide=${stopPropagation}
|
||||
@wa-after-hide=${stopPropagation}
|
||||
@wa-after-show=${stopPropagation}
|
||||
>${item.description}</ha-tooltip
|
||||
> `
|
||||
: nothing}
|
||||
<ha-svg-icon
|
||||
slot="end"
|
||||
class="plus"
|
||||
.path=${mdiPlus}
|
||||
></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTarget = memoizeOne((target?: Target) => {
|
||||
if (!target) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
return html`<div class="selected-target">
|
||||
${this._getSelectedTargetIcon(target[0], target[1])}
|
||||
<div class="label">${target[2]}</div>
|
||||
</div>`;
|
||||
});
|
||||
|
||||
private _getSelectedTargetIcon(
|
||||
targetType: string,
|
||||
targetId: string | undefined
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!targetId) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (targetType === "floor") {
|
||||
return html`<ha-floor-icon
|
||||
.floor=${this.hass.floors[targetId]}
|
||||
></ha-floor-icon>`;
|
||||
}
|
||||
|
||||
if (targetType === "area" && this.hass.areas[targetId]) {
|
||||
const area = this.hass.areas[targetId];
|
||||
if (area.icon) {
|
||||
return html`<ha-icon .icon=${area.icon}></ha-icon>`;
|
||||
}
|
||||
return html`<ha-svg-icon .path=${mdiTextureBox}></ha-svg-icon>`;
|
||||
}
|
||||
|
||||
if (targetType === "device" && this.hass.devices[targetId]) {
|
||||
const device = this.hass.devices[targetId];
|
||||
const configEntry = device.primary_config_entry
|
||||
? this.configEntryLookup[device.primary_config_entry]
|
||||
: undefined;
|
||||
const domain = configEntry?.domain;
|
||||
|
||||
if (domain) {
|
||||
return html`<ha-domain-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetType === "entity" && this.hass.states[targetId]) {
|
||||
const stateObj = this.hass.states[targetId];
|
||||
if (stateObj) {
|
||||
return html`<state-badge
|
||||
.stateObj=${stateObj}
|
||||
.hass=${this.hass}
|
||||
.stateColor=${false}
|
||||
></state-badge>`;
|
||||
}
|
||||
}
|
||||
|
||||
if (targetType === "label") {
|
||||
const label = this.getLabel(targetId);
|
||||
if (label?.icon) {
|
||||
return html`<ha-icon .icon=${label.icon}></ha-icon>`;
|
||||
}
|
||||
return html`<ha-svg-icon .path=${mdiLabel}></ha-svg-icon>`;
|
||||
}
|
||||
|
||||
return nothing;
|
||||
}
|
||||
|
||||
private _selected(ev) {
|
||||
const item = ev.currentTarget;
|
||||
fireEvent(this, "value-changed", {
|
||||
value: item.value,
|
||||
});
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _onItemsScroll(ev) {
|
||||
const top = ev.target.scrollTop ?? 0;
|
||||
this._itemsScrolled = top > 0;
|
||||
}
|
||||
|
||||
public override scrollTo(options?: ScrollToOptions): void;
|
||||
|
||||
public override scrollTo(x: number, y: number): void;
|
||||
|
||||
public override scrollTo(
|
||||
xOrOptions?: number | ScrollToOptions,
|
||||
y?: number
|
||||
): void {
|
||||
if (typeof xOrOptions === "number") {
|
||||
this._itemsDiv?.scrollTo(xOrOptions, y!);
|
||||
} else {
|
||||
this._itemsDiv?.scrollTo(xOrOptions);
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
}
|
||||
.items.blank {
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
background-color: var(--ha-color-surface-default);
|
||||
align-items: center;
|
||||
color: var(--ha-color-text-secondary);
|
||||
padding: var(--ha-space-0);
|
||||
margin: var(--ha-space-3) var(--ha-space-4)
|
||||
max(var(--safe-area-inset-bottom), var(--ha-space-3));
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.items.error {
|
||||
background-color: var(--ha-color-fill-danger-quiet-resting);
|
||||
color: var(--ha-color-on-danger-normal);
|
||||
}
|
||||
.items ha-md-list {
|
||||
--md-list-item-two-line-container-height: var(--ha-space-12);
|
||||
--md-list-item-leading-space: var(--ha-space-3);
|
||||
--md-list-item-trailing-space: var(--md-list-item-leading-space);
|
||||
--md-list-item-bottom-space: var(--ha-space-2);
|
||||
--md-list-item-top-space: var(--md-list-item-bottom-space);
|
||||
--md-list-item-supporting-text-font: var(--ha-font-family-body);
|
||||
--ha-md-list-item-gap: var(--ha-space-3);
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-0) var(--ha-space-4);
|
||||
}
|
||||
.items ha-md-list ha-md-list-item {
|
||||
border-radius: var(--ha-border-radius-lg);
|
||||
border: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
|
||||
.items ha-md-list {
|
||||
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
|
||||
}
|
||||
|
||||
.items .item-headline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-1);
|
||||
min-height: var(--ha-space-9);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.items-title {
|
||||
position: sticky;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
padding-top: var(--ha-space-2);
|
||||
padding-bottom: var(--ha-space-2);
|
||||
padding-inline-start: var(--ha-space-8);
|
||||
padding-inline-end: var(--ha-space-3);
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
background-color: var(--card-background-color);
|
||||
}
|
||||
ha-bottom-sheet .items-title {
|
||||
padding-top: var(--ha-space-3);
|
||||
}
|
||||
.scrolled .items-title:first-of-type {
|
||||
box-shadow: var(--bar-box-shadow);
|
||||
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
|
||||
}
|
||||
|
||||
ha-icon-next {
|
||||
width: var(--ha-space-6);
|
||||
}
|
||||
|
||||
ha-svg-icon.plus {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.selected-target {
|
||||
display: inline-flex;
|
||||
gap: var(--ha-space-1);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: var(--ha-border-radius-md);
|
||||
background: var(--ha-color-fill-neutral-normal-resting);
|
||||
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
|
||||
color: var(--ha-color-on-neutral-normal);
|
||||
overflow: hidden;
|
||||
}
|
||||
.selected-target .label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.selected-target ha-icon,
|
||||
.selected-target ha-svg-icon,
|
||||
.selected-target state-badge,
|
||||
.selected-target ha-domain-icon {
|
||||
display: flex;
|
||||
padding: var(--ha-space-1) 0;
|
||||
}
|
||||
|
||||
.selected-target state-badge {
|
||||
--mdc-icon-size: 20px;
|
||||
}
|
||||
.selected-target state-badge,
|
||||
.selected-target ha-domain-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-add-items": HaAutomationAddItems;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -8,11 +8,13 @@ import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { Condition } from "../../../../data/automation";
|
||||
import { expandConditionWithShorthand } from "../../../../data/automation";
|
||||
import type { ConditionDescription } from "../../../../data/condition";
|
||||
import { COLLAPSIBLE_CONDITION_ELEMENTS } from "../../../../data/condition";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { editorStyles, indentStyle } from "../styles";
|
||||
import type { ConditionElement } from "./ha-automation-condition-row";
|
||||
import "./types/ha-automation-condition-platform";
|
||||
|
||||
@customElement("ha-automation-condition-editor")
|
||||
export default class HaAutomationConditionEditor extends LitElement {
|
||||
@@ -35,6 +37,8 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
|
||||
false;
|
||||
|
||||
@property({ attribute: false }) public description?: ConditionDescription;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
|
||||
@query(COLLAPSIBLE_CONDITION_ELEMENTS.join(", "))
|
||||
@@ -83,7 +87,14 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${dynamicElement(
|
||||
${this.description
|
||||
? html`<ha-automation-condition-platform
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.description=${this.description}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-condition-platform>`
|
||||
: dynamicElement(
|
||||
`ha-automation-condition-${condition.condition}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
|
||||
@@ -32,6 +32,7 @@ import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-automation-row";
|
||||
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-condition-icon";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
@@ -44,10 +45,8 @@ import type {
|
||||
} from "../../../../data/automation";
|
||||
import { isCondition, testCondition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
CONDITION_ICONS,
|
||||
} from "../../../../data/condition";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
@@ -130,6 +129,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" })
|
||||
public optionsInSidebar = false;
|
||||
|
||||
@@ -179,11 +181,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
private _renderRow() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
<ha-condition-icon
|
||||
slot="leading-icon"
|
||||
class="condition-icon"
|
||||
.path=${CONDITION_ICONS[this.condition.condition]}
|
||||
></ha-svg-icon>
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
@@ -395,9 +397,14 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
<ha-automation-condition-editor
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.description=${this.conditionDescriptions[
|
||||
this.condition.condition
|
||||
]}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
||||
.uiSupported=${this._uiSupported(
|
||||
this._getType(this.condition, this.conditionDescriptions)
|
||||
)}
|
||||
.narrow=${this.narrow}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-condition-editor>`
|
||||
@@ -476,7 +483,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.disabled=${this.disabled}
|
||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
||||
.uiSupported=${this._uiSupported(
|
||||
this._getType(this.condition, this.conditionDescriptions)
|
||||
)}
|
||||
indent
|
||||
.selected=${this._selected}
|
||||
.narrow=${this.narrow}
|
||||
@@ -786,7 +795,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
cut: this._cutCondition,
|
||||
test: this._testCondition,
|
||||
config: sidebarCondition,
|
||||
uiSupported: this._uiSupported(sidebarCondition.condition),
|
||||
uiSupported: this._uiSupported(
|
||||
this._getType(sidebarCondition, this.conditionDescriptions)
|
||||
),
|
||||
description: this.conditionDescriptions[sidebarCondition.condition],
|
||||
yamlMode: this._yamlMode,
|
||||
} satisfies ConditionSidebarConfig);
|
||||
this._selected = true;
|
||||
@@ -802,6 +814,16 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _getType = memoizeOne(
|
||||
(condition: Condition, conditionDescriptions: ConditionDescriptions) => {
|
||||
if (condition.condition in conditionDescriptions) {
|
||||
return "platform";
|
||||
}
|
||||
|
||||
return condition.condition;
|
||||
}
|
||||
);
|
||||
|
||||
private _uiSupported = memoizeOne(
|
||||
(type: string) =>
|
||||
customElements.get(`ha-automation-condition-${type}`) !== undefined
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
@@ -12,11 +14,18 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
import {
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type AutomationClipboard,
|
||||
type Condition,
|
||||
} from "../../../../data/automation";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
subscribeConditions,
|
||||
} from "../../../../data/condition";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
@@ -25,10 +34,9 @@ import {
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import "./ha-automation-condition-row";
|
||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
|
||||
@customElement("ha-automation-condition")
|
||||
export default class HaAutomationCondition extends LitElement {
|
||||
export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public conditions!: Condition[];
|
||||
@@ -46,6 +54,8 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
@state() private _rowSortSelected?: number;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
@@ -64,6 +74,26 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
private _conditionKeys = new WeakMap<Condition, string>();
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeConditions(this.hass, (conditions) =>
|
||||
this._addConditions(conditions)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _addConditions(conditions: ConditionDescriptions) {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("conditions")) {
|
||||
return;
|
||||
@@ -168,6 +198,7 @@ export default class HaAutomationCondition extends LitElement {
|
||||
.last=${idx === this.conditions.length - 1}
|
||||
.totalConditions=${this.conditions.length}
|
||||
.condition=${cond}
|
||||
.conditionDescriptions=${this._conditionDescriptions}
|
||||
.disabled=${this.disabled}
|
||||
.narrow=${this.narrow}
|
||||
@duplicate=${this._duplicateCondition}
|
||||
@@ -231,12 +262,17 @@ export default class HaAutomationCondition extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
private _addCondition = (value) => {
|
||||
private _addCondition = (value: string, target?: HassServiceTarget) => {
|
||||
let conditions: Condition[];
|
||||
if (value === PASTE_VALUE) {
|
||||
conditions = this.conditions.concat(
|
||||
deepClone(this._clipboard!.condition)
|
||||
);
|
||||
} else if (isDynamic(value)) {
|
||||
conditions = this.conditions.concat({
|
||||
condition: getValueFromDynamic(value),
|
||||
target,
|
||||
});
|
||||
} else {
|
||||
const condition = value as Condition["condition"];
|
||||
const elClass = customElements.get(
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import type { PlatformCondition } from "../../../../../data/automation";
|
||||
import {
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
type ConditionDescription,
|
||||
} from "../../../../../data/condition";
|
||||
import type { IntegrationManifest } from "../../../../../data/integration";
|
||||
import { fetchIntegrationManifest } from "../../../../../data/integration";
|
||||
import type { TargetSelector } from "../../../../../data/selector";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||
|
||||
const showOptionalToggle = (field: ConditionDescription["fields"][string]) =>
|
||||
field.selector &&
|
||||
!field.required &&
|
||||
!("boolean" in field.selector && field.default);
|
||||
|
||||
@customElement("ha-automation-condition-platform")
|
||||
export class HaPlatformCondition extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public condition!: PlatformCondition;
|
||||
|
||||
@property({ attribute: false }) public description?: ConditionDescription;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _checkedKeys = new Set();
|
||||
|
||||
@state() private _manifest?: IntegrationManifest;
|
||||
|
||||
public static get defaultConfig(): PlatformCondition {
|
||||
return { condition: "" };
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
this.hass.loadBackendTranslation("selector");
|
||||
}
|
||||
if (!changedProperties.has("condition")) {
|
||||
return;
|
||||
}
|
||||
const oldValue = changedProperties.get("condition") as
|
||||
| undefined
|
||||
| this["condition"];
|
||||
|
||||
// Fetch the manifest if we have a condition selected and the condition domain changed.
|
||||
// If no condition is selected, clear the manifest.
|
||||
if (this.condition?.condition) {
|
||||
const domain = getConditionDomain(this.condition.condition);
|
||||
|
||||
const oldDomain = getConditionDomain(oldValue?.condition || "");
|
||||
|
||||
if (domain !== oldDomain) {
|
||||
this._fetchManifest(domain);
|
||||
}
|
||||
} else {
|
||||
this._manifest = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const domain = getConditionDomain(this.condition.condition);
|
||||
const conditionName = getConditionObjectId(this.condition.condition);
|
||||
|
||||
const description = this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.description`
|
||||
);
|
||||
|
||||
const conditionDesc = this.description;
|
||||
|
||||
const shouldRenderDataYaml = !conditionDesc?.fields;
|
||||
|
||||
const hasOptional = Boolean(
|
||||
conditionDesc?.fields &&
|
||||
Object.values(conditionDesc.fields).some((field) =>
|
||||
showOptionalToggle(field)
|
||||
)
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="description">
|
||||
${description ? html`<p>${description}</p>` : nothing}
|
||||
${this._manifest
|
||||
? html`<a
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._manifest.domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-icon-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
</div>
|
||||
${conditionDesc && "target" in conditionDesc
|
||||
? html`<ha-settings-row narrow>
|
||||
${hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: nothing}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_secondary"
|
||||
)}</span
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._targetSelector(conditionDesc.target)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this.condition?.target}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: nothing}
|
||||
${shouldRenderDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.service-control.action_data"
|
||||
)}
|
||||
.name=${"data"}
|
||||
.readOnly=${this.disabled}
|
||||
.defaultValue=${this.condition?.options}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: Object.entries(conditionDesc.fields).map(([fieldName, dataField]) =>
|
||||
this._renderField(
|
||||
fieldName,
|
||||
dataField,
|
||||
hasOptional,
|
||||
domain,
|
||||
conditionName
|
||||
)
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetSelector = memoizeOne(
|
||||
(targetSelector: TargetSelector["target"] | null | undefined) =>
|
||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
||||
);
|
||||
|
||||
private _renderField = (
|
||||
fieldName: string,
|
||||
dataField: ConditionDescription["fields"][string],
|
||||
hasOptional: boolean,
|
||||
domain: string | undefined,
|
||||
conditionName: string | undefined
|
||||
) => {
|
||||
const selector = dataField?.selector ?? { text: null };
|
||||
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
|
||||
return dataField.selector
|
||||
? html`<ha-settings-row narrow>
|
||||
${!showOptional
|
||||
? hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: nothing
|
||||
: html`<ha-checkbox
|
||||
.key=${fieldName}
|
||||
.checked=${this._checkedKeys.has(fieldName) ||
|
||||
(this.condition?.options &&
|
||||
this.condition.options[fieldName] !== undefined)}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.name`
|
||||
) || conditionName}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.description`
|
||||
)}</span
|
||||
>
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(fieldName) &&
|
||||
(!this.condition?.options ||
|
||||
this.condition.options[fieldName] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${selector}
|
||||
.context=${this._generateContext(dataField)}
|
||||
.key=${fieldName}
|
||||
@value-changed=${this._dataChanged}
|
||||
.value=${this.condition?.options
|
||||
? this.condition.options[fieldName]
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: nothing;
|
||||
};
|
||||
|
||||
private _generateContext(
|
||||
field: ConditionDescription["fields"][string]
|
||||
): Record<string, any> | undefined {
|
||||
if (!field.context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
for (const [context_key, data_key] of Object.entries(field.context)) {
|
||||
context[context_key] =
|
||||
data_key === "target"
|
||||
? this.condition.target
|
||||
: this.condition.options?.[data_key];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private _dataChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.isValid === false) {
|
||||
// Don't clear an object selector that returns invalid YAML
|
||||
return;
|
||||
}
|
||||
const key = (ev.currentTarget as any).key;
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
this.condition?.options?.[key] === value ||
|
||||
((!this.condition?.options || !(key in this.condition.options)) &&
|
||||
(value === "" || value === undefined))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = { ...this.condition?.options, [key]: value };
|
||||
|
||||
if (
|
||||
value === "" ||
|
||||
value === undefined ||
|
||||
(typeof value === "object" && !Object.keys(value).length)
|
||||
) {
|
||||
delete options[key];
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
options,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _targetChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
target: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _checkboxChanged(ev) {
|
||||
const checked = ev.currentTarget.checked;
|
||||
const key = ev.currentTarget.key;
|
||||
let options;
|
||||
|
||||
if (checked) {
|
||||
this._checkedKeys.add(key);
|
||||
const field =
|
||||
this.description &&
|
||||
Object.entries(this.description).find(([k, _value]) => k === key)?.[1];
|
||||
let defaultValue = field?.default;
|
||||
|
||||
if (
|
||||
defaultValue == null &&
|
||||
field?.selector &&
|
||||
"constant" in field.selector
|
||||
) {
|
||||
defaultValue = field.selector.constant?.value;
|
||||
}
|
||||
|
||||
if (
|
||||
defaultValue == null &&
|
||||
field?.selector &&
|
||||
"boolean" in field.selector
|
||||
) {
|
||||
defaultValue = false;
|
||||
}
|
||||
|
||||
if (defaultValue != null) {
|
||||
options = {
|
||||
...this.condition?.options,
|
||||
[key]: defaultValue,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
this._checkedKeys.delete(key);
|
||||
options = { ...this.condition?.options };
|
||||
delete options[key];
|
||||
}
|
||||
if (options) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
options,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.requestUpdate("_checkedKeys");
|
||||
}
|
||||
|
||||
private _localizeValueCallback = (key: string) => {
|
||||
if (!this.condition?.condition) {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${computeDomain(this.condition.condition)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
private async _fetchManifest(integration: string) {
|
||||
this._manifest = undefined;
|
||||
try {
|
||||
this._manifest = await fetchIntegrationManifest(this.hass, integration);
|
||||
} catch (_err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Unable to fetch integration manifest for ${integration}`);
|
||||
// Ignore if loading manifest fails. Probably bad JSON in manifest
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-settings-row {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
ha-settings-row[narrow] {
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-settings-row {
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
);
|
||||
}
|
||||
ha-service-picker,
|
||||
ha-entity-picker,
|
||||
ha-yaml-editor {
|
||||
display: block;
|
||||
margin: 0 var(--ha-space-4);
|
||||
}
|
||||
ha-yaml-editor {
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
p {
|
||||
margin: 0 var(--ha-space-4);
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
:host([hide-picker]) p {
|
||||
padding-top: 0;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
ha-checkbox {
|
||||
margin-left: calc(var(--ha-space-4) * -1);
|
||||
margin-inline-start: calc(var(--ha-space-4) * -1);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.help-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.description {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 2px;
|
||||
padding-inline-end: 2px;
|
||||
padding-inline-start: initial;
|
||||
}
|
||||
.description p {
|
||||
direction: ltr;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-condition-platform": HaPlatformCondition;
|
||||
}
|
||||
}
|
||||
@@ -299,7 +299,6 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.automation.picker.headers.area"),
|
||||
defaultHidden: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export const PASTE_VALUE = "__paste__";
|
||||
|
||||
export interface AddAutomationElementDialogParams {
|
||||
type: "trigger" | "condition" | "action";
|
||||
add: (key: string) => void;
|
||||
add: (key: string, target?: HassServiceTarget) => void;
|
||||
clipboardItem: string | undefined;
|
||||
}
|
||||
const loadDialog = () => import("./add-automation-element-dialog");
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
property,
|
||||
query,
|
||||
state,
|
||||
} from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
@@ -17,8 +9,10 @@ import "../../../../components/ha-dialog-header";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-divider";
|
||||
import { haStyleScrollbar } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { ScrollableFadeMixin } from "../../../../mixins/scrollable-fade-mixin";
|
||||
|
||||
export interface SidebarOverflowMenuEntry {
|
||||
clickAction: () => void;
|
||||
@@ -31,7 +25,9 @@ export interface SidebarOverflowMenuEntry {
|
||||
export type SidebarOverflowMenu = (SidebarOverflowMenuEntry | "separator")[];
|
||||
|
||||
@customElement("ha-automation-sidebar-card")
|
||||
export default class HaAutomationSidebarCard extends LitElement {
|
||||
export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
|
||||
LitElement
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean, attribute: "wide" }) public isWide = false;
|
||||
@@ -42,23 +38,10 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _contentScrolled = false;
|
||||
|
||||
@state() private _contentScrollable = false;
|
||||
|
||||
@query(".card-content") private _contentElement!: HTMLDivElement;
|
||||
|
||||
private _contentSize = new ResizeController(this, {
|
||||
target: null,
|
||||
callback: (entries) => {
|
||||
if (entries[0]?.target) {
|
||||
this._canScrollDown(entries[0].target);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
protected firstUpdated(_changedProperties: PropertyValues): void {
|
||||
this._contentSize.observe(this._contentElement);
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._contentElement;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -70,9 +53,7 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
yaml: this.yamlMode,
|
||||
})}
|
||||
>
|
||||
<ha-dialog-header
|
||||
class=${classMap({ scrolled: this._contentScrolled })}
|
||||
>
|
||||
<ha-dialog-header>
|
||||
<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.common.close")}
|
||||
@@ -107,34 +88,14 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
>
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing}
|
||||
<div class="card-content" @scroll=${this._onScroll}>
|
||||
<div class="card-content ha-scrollbar">
|
||||
<slot></slot>
|
||||
${this.renderScrollableFades(this.isWide)}
|
||||
</div>
|
||||
<div
|
||||
class=${classMap({ fade: true, scrollable: this._contentScrollable })}
|
||||
></div>
|
||||
</ha-card>
|
||||
`;
|
||||
}
|
||||
|
||||
@eventOptions({ passive: true })
|
||||
private _onScroll(ev) {
|
||||
const top = ev.target.scrollTop ?? 0;
|
||||
this._contentScrolled = top > 0;
|
||||
|
||||
this._canScrollDown(ev.target);
|
||||
}
|
||||
|
||||
private _canScrollDown(element: HTMLElement) {
|
||||
const safeAreaInsetBottom =
|
||||
parseFloat(
|
||||
getComputedStyle(element).getPropertyValue("--safe-area-inset-bottom")
|
||||
) || 0;
|
||||
this._contentScrollable =
|
||||
(element.scrollHeight ?? 0) - (element.clientHeight ?? 0) >
|
||||
(element.scrollTop ?? 0) + safeAreaInsetBottom + 16;
|
||||
}
|
||||
|
||||
private _closeSidebar() {
|
||||
fireEvent(this, "close-sidebar");
|
||||
}
|
||||
@@ -144,7 +105,11 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
@@ -168,8 +133,6 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
|
||||
ha-dialog-header {
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
box-shadow: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
position: relative;
|
||||
@@ -179,32 +142,6 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
ha-dialog-header.scrolled {
|
||||
box-shadow: var(--bar-box-shadow);
|
||||
}
|
||||
|
||||
.fade {
|
||||
position: absolute;
|
||||
bottom: 1px;
|
||||
left: 1px;
|
||||
right: 1px;
|
||||
height: 16px;
|
||||
pointer-events: none;
|
||||
transition: box-shadow 180ms ease-in-out;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
transform: rotate(180deg);
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
|
||||
.fade.scrollable {
|
||||
box-shadow: var(--bar-box-shadow);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -213,17 +150,18 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.fade {
|
||||
bottom: 0;
|
||||
border-radius: var(--ha-border-radius-square);
|
||||
.fade-top {
|
||||
top: var(--ha-space-17);
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
.card-content {
|
||||
padding-bottom: 42px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -16,11 +16,16 @@ import { classMap } from "lit/directives/class-map";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||
import {
|
||||
testCondition,
|
||||
type ConditionSidebarConfig,
|
||||
import type {
|
||||
LegacyCondition,
|
||||
ConditionSidebarConfig,
|
||||
} from "../../../../data/automation";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import { testCondition } from "../../../../data/automation";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
} from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { isMac } from "../../../../util/is_mac";
|
||||
@@ -84,14 +89,25 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
"ui.panel.config.automation.editor.conditions.condition"
|
||||
);
|
||||
|
||||
const domain =
|
||||
"condition" in this.config.config &&
|
||||
getConditionDomain(this.config.config.condition);
|
||||
const conditionName =
|
||||
"condition" in this.config.config &&
|
||||
getConditionObjectId(this.config.config.condition);
|
||||
|
||||
const title =
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${type}.label`
|
||||
) || type;
|
||||
`ui.panel.config.automation.editor.conditions.type.${type as LegacyCondition["condition"]}.label`
|
||||
) ||
|
||||
this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.name`
|
||||
) ||
|
||||
type;
|
||||
|
||||
const description = isBuildingBlock
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${type}.description.picker`
|
||||
`ui.panel.config.automation.editor.conditions.type.${type as LegacyCondition["condition"]}.description.picker`
|
||||
)
|
||||
: "";
|
||||
|
||||
@@ -282,6 +298,7 @@ export default class HaAutomationSidebarCondition extends LitElement {
|
||||
class="sidebar-editor"
|
||||
.hass=${this.hass}
|
||||
.condition=${this.config.config}
|
||||
.description=${this.config.description}
|
||||
.yamlMode=${this.yamlMode}
|
||||
.uiSupported=${this.config.uiSupported}
|
||||
@value-changed=${this._valueChangedSidebar}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -174,13 +175,14 @@ export default class HaAutomationTrigger extends SubscribeMixin(LitElement) {
|
||||
});
|
||||
}
|
||||
|
||||
private _addTrigger = (value: string) => {
|
||||
private _addTrigger = (value: string, target?: HassServiceTarget) => {
|
||||
let triggers: Trigger[];
|
||||
if (value === PASTE_VALUE) {
|
||||
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger));
|
||||
} else if (isDynamic(value)) {
|
||||
triggers = this.triggers.concat({
|
||||
trigger: getValueFromDynamic(value),
|
||||
target,
|
||||
});
|
||||
} else {
|
||||
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { mdiOpenInNew } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import "../../../components/ha-analytics";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-settings-row";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { Analytics } from "../../../data/analytics";
|
||||
import {
|
||||
getAnalyticsDetails,
|
||||
@@ -17,6 +13,8 @@ import {
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { documentationUrl } from "../../../util/documentation-url";
|
||||
import type { HaSwitch } from "../../../components/ha-switch";
|
||||
import "../../../components/ha-alert";
|
||||
|
||||
@customElement("ha-config-analytics")
|
||||
class ConfigAnalytics extends LitElement {
|
||||
@@ -34,10 +32,22 @@ class ConfigAnalytics extends LitElement {
|
||||
: undefined;
|
||||
|
||||
return html`
|
||||
<ha-card outlined>
|
||||
<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize("ui.panel.config.analytics.header") ||
|
||||
"Home Assistant analytics"}
|
||||
>
|
||||
<div class="card-content">
|
||||
${error ? html`<div class="error">${error}</div>` : ""}
|
||||
<p>${this.hass.localize("ui.panel.config.analytics.intro")}</p>
|
||||
${error ? html`<div class="error">${error}</div>` : nothing}
|
||||
<p>
|
||||
${this.hass.localize("ui.panel.config.analytics.intro")}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize("ui.panel.config.analytics.learn_more")}</a
|
||||
>.
|
||||
</p>
|
||||
<ha-analytics
|
||||
translation_key_panel="config"
|
||||
@analytics-preferences-changed=${this._preferencesChanged}
|
||||
@@ -45,26 +55,59 @@ class ConfigAnalytics extends LitElement {
|
||||
.analytics=${this._analyticsDetails}
|
||||
></ha-analytics>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<ha-button @click=${this._save}>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.core.section.core.core_config.save_button"
|
||||
)}
|
||||
</ha-button>
|
||||
</div>
|
||||
</ha-card>
|
||||
<div class="footer">
|
||||
<ha-button
|
||||
size="small"
|
||||
appearance="plain"
|
||||
href=${documentationUrl(this.hass, "/integrations/analytics/")}
|
||||
${this._analyticsDetails &&
|
||||
"snapshots" in this._analyticsDetails.preferences
|
||||
? html`<ha-card
|
||||
outlined
|
||||
.header=${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.header"
|
||||
)}
|
||||
>
|
||||
<div class="card-content">
|
||||
<p>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.info"
|
||||
)}
|
||||
<a
|
||||
href=${documentationUrl(this.hass, "/device-database/")}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.learn_more"
|
||||
)}</a
|
||||
>.
|
||||
</p>
|
||||
<ha-alert
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.alert.title"
|
||||
)}
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.analytics.preferences.snapshots.alert.content"
|
||||
)}</ha-alert
|
||||
>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.analytics.learn_more")}
|
||||
</ha-button>
|
||||
<ha-settings-row>
|
||||
<span slot="heading" data-for="snapshots">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.analytics.preferences.snapshots.title`
|
||||
)}
|
||||
</span>
|
||||
<span slot="description" data-for="snapshots">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.analytics.preferences.snapshots.description`
|
||||
)}
|
||||
</span>
|
||||
<ha-switch
|
||||
@change=${this._handleDeviceRowClick}
|
||||
.checked=${!!this._analyticsDetails?.preferences.snapshots}
|
||||
.disabled=${this._analyticsDetails === undefined}
|
||||
name="snapshots"
|
||||
>
|
||||
</ha-switch>
|
||||
</ha-settings-row>
|
||||
</div>
|
||||
</ha-card>`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -96,11 +139,25 @@ class ConfigAnalytics extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _handleDeviceRowClick(ev: Event) {
|
||||
const target = ev.target as HaSwitch;
|
||||
|
||||
this._analyticsDetails = {
|
||||
...this._analyticsDetails!,
|
||||
preferences: {
|
||||
...this._analyticsDetails!.preferences,
|
||||
snapshots: target.checked,
|
||||
},
|
||||
};
|
||||
this._save();
|
||||
}
|
||||
|
||||
private _preferencesChanged(event: CustomEvent): void {
|
||||
this._analyticsDetails = {
|
||||
...this._analyticsDetails!,
|
||||
preferences: event.detail.preferences,
|
||||
};
|
||||
this._save();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
@@ -117,21 +174,10 @@ class ConfigAnalytics extends LitElement {
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
.card-actions {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
ha-card:not(:first-of-type) {
|
||||
margin-top: 24px;
|
||||
}
|
||||
.footer {
|
||||
padding: 32px 0 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ha-button[size="small"] ha-svg-icon {
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
`, // row-reverse so we tab first to "save"
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
import { titleCase } from "../../../../common/string/title-case";
|
||||
import "../../../../components/ha-card";
|
||||
import type { DeviceRegistryEntry } from "../../../../data/device_registry";
|
||||
@@ -9,16 +11,61 @@ import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { createSearchParam } from "../../../../common/url/search-params";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import "../../../../components/ha-icon";
|
||||
import "../../../../components/ha-label";
|
||||
import type { LabelRegistryEntry } from "../../../../data/label_registry";
|
||||
import { subscribeLabelRegistry } from "../../../../data/label_registry";
|
||||
import { computeCssColor } from "../../../../common/color/compute-color";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
|
||||
@customElement("ha-device-info-card")
|
||||
export class HaDeviceCard extends LitElement {
|
||||
export class HaDeviceCard extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public device!: DeviceRegistryEntry;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@state() private _labelRegistry?: LabelRegistryEntry[];
|
||||
|
||||
private _labelsData = memoizeOne(
|
||||
(
|
||||
labels: LabelRegistryEntry[] | undefined,
|
||||
labelIds: string[],
|
||||
language: string
|
||||
): {
|
||||
map: Map<string, LabelRegistryEntry>;
|
||||
ids: string[];
|
||||
} => {
|
||||
const map = labels
|
||||
? new Map(labels.map((label) => [label.label_id, label]))
|
||||
: new Map<string, LabelRegistryEntry>();
|
||||
const ids = [...labelIds].sort((labelA, labelB) =>
|
||||
stringCompare(
|
||||
map.get(labelA)?.name || labelA,
|
||||
map.get(labelB)?.name || labelB,
|
||||
language
|
||||
)
|
||||
);
|
||||
return { map, ids };
|
||||
}
|
||||
);
|
||||
|
||||
public hassSubscribe() {
|
||||
return [
|
||||
subscribeLabelRegistry(this.hass.connection, (labels) => {
|
||||
this._labelRegistry = labels;
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const { map: labelMap, ids: labels } = this._labelsData(
|
||||
this._labelRegistry,
|
||||
this.device.labels,
|
||||
this.hass.locale.language
|
||||
);
|
||||
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
@@ -58,7 +105,7 @@ export class HaDeviceCard extends LitElement {
|
||||
<span class="hub"
|
||||
><a
|
||||
href="/config/devices/device/${this.device.via_device_id}"
|
||||
>${this._computeDeviceNameDislay(
|
||||
>${this._computeDeviceNameDisplay(
|
||||
this.device.via_device_id
|
||||
)}</a
|
||||
></span
|
||||
@@ -126,6 +173,34 @@ export class HaDeviceCard extends LitElement {
|
||||
</div>
|
||||
`
|
||||
)}
|
||||
${labels.length > 0
|
||||
? html`
|
||||
<div class="extra-info labels">
|
||||
${labels.map((labelId) => {
|
||||
const label = labelMap.get(labelId);
|
||||
const color =
|
||||
label?.color && typeof label.color === "string"
|
||||
? computeCssColor(label.color)
|
||||
: undefined;
|
||||
return html`
|
||||
<ha-label
|
||||
style=${color ? `--color: ${color}` : ""}
|
||||
.description=${label?.description}
|
||||
>
|
||||
${label?.icon
|
||||
? html`<ha-icon
|
||||
slot="icon"
|
||||
.icon=${label.icon}
|
||||
></ha-icon>`
|
||||
: nothing}
|
||||
${label?.name || labelId}
|
||||
</ha-label>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot name="actions"></slot>
|
||||
@@ -139,7 +214,7 @@ export class HaDeviceCard extends LitElement {
|
||||
);
|
||||
}
|
||||
|
||||
private _computeDeviceNameDislay(deviceId) {
|
||||
private _computeDeviceNameDisplay(deviceId: string) {
|
||||
const device = this.hass.devices[deviceId];
|
||||
return device
|
||||
? computeDeviceNameDisplay(device, this.hass)
|
||||
@@ -162,8 +237,26 @@ export class HaDeviceCard extends LitElement {
|
||||
.device {
|
||||
width: 30%;
|
||||
}
|
||||
.labels {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--ha-space-1);
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
.labels ha-label {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
flex: 0 1 auto;
|
||||
}
|
||||
ha-label {
|
||||
--ha-label-background-color: var(--color, var(--grey-color));
|
||||
--ha-label-background-opacity: 0.5;
|
||||
--ha-label-text-color: var(--primary-text-color);
|
||||
--ha-label-icon-color: var(--primary-text-color);
|
||||
}
|
||||
.extra-info {
|
||||
margin-top: 8px;
|
||||
margin-top: var(--ha-space-2);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.manuf,
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiCancel,
|
||||
mdiChevronRight,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiMenuDown,
|
||||
mdiPlus,
|
||||
mdiTextureBox,
|
||||
mdiCancel,
|
||||
mdiDelete,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
|
||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
|
||||
@@ -68,8 +68,8 @@ import type {
|
||||
DeviceRegistryEntry,
|
||||
} from "../../../data/device_registry";
|
||||
import {
|
||||
updateDeviceRegistryEntry,
|
||||
removeConfigEntryFromDevice,
|
||||
updateDeviceRegistryEntry,
|
||||
} from "../../../data/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||
import {
|
||||
@@ -86,8 +86,8 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../types";
|
||||
@@ -318,7 +318,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
|
||||
})
|
||||
);
|
||||
|
||||
const deviceEntityLookup: DeviceEntityLookup = {};
|
||||
const deviceEntityLookup: DeviceEntityLookup<EntityRegistryEntry> = {};
|
||||
for (const entity of entities) {
|
||||
if (!entity.device_id) {
|
||||
continue;
|
||||
|
||||
@@ -25,10 +25,6 @@ import { computeCssColor } from "../../../common/color/compute-color";
|
||||
import { storage } from "../../../common/decorators/storage";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
|
||||
import {
|
||||
DEFAULT_ENTITY_NAME,
|
||||
type EntityNameItem,
|
||||
} from "../../../common/entity/compute_entity_name_display";
|
||||
import { navigate } from "../../../common/navigate";
|
||||
import type {
|
||||
LocalizeFunc,
|
||||
@@ -126,11 +122,6 @@ import {
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
|
||||
const HELPER_ENTITY_NAME: EntityNameItem[] = [
|
||||
{ type: "area" },
|
||||
...DEFAULT_ENTITY_NAME,
|
||||
];
|
||||
|
||||
interface HelperItem {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -514,7 +505,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
|
||||
return {
|
||||
id: entityState.entity_id,
|
||||
name: this._formatHelperName(entityState),
|
||||
name: entityState.attributes.friendly_name || "",
|
||||
entity_id: entityState.entity_id,
|
||||
editable:
|
||||
configEntry !== undefined || entityState.attributes.editable,
|
||||
@@ -593,14 +584,6 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
);
|
||||
|
||||
private _formatHelperName(stateObj: HassEntity): string {
|
||||
const formatted =
|
||||
this.hass.formatEntityName(stateObj, HELPER_ENTITY_NAME) || "";
|
||||
return (
|
||||
formatted || stateObj.attributes.friendly_name || stateObj.entity_id || ""
|
||||
);
|
||||
}
|
||||
|
||||
private _labelsForEntity(entityId: string): string[] {
|
||||
return (
|
||||
this.hass.entities[entityId]?.labels ||
|
||||
|
||||
@@ -12,7 +12,7 @@ import "../../../lovelace/editor/dashboard-strategy-editor/hui-dashboard-strateg
|
||||
import type { LovelaceDashboardConfigureStrategyDialogParams } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-configure-strategy")
|
||||
export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LovelaceDashboardConfigureStrategyDialogParams;
|
||||
@@ -97,6 +97,6 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-lovelace-dashboard-configure-strategy": DialogLovelaceDashboardDetail;
|
||||
"dialog-lovelace-dashboard-configure-strategy": DialogLovelaceDashboardConfigureStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,13 @@ import "../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||
import type {
|
||||
LovelaceDashboard,
|
||||
LovelaceDashboardCreateParams,
|
||||
LovelaceDashboardMutableParams,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
@@ -61,9 +58,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
if (!this._params || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
const defaultPanelUrlPath =
|
||||
this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
|
||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||
const isLovelaceDashboard = this._params.urlPath === "lovelace";
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -88,9 +85,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
||||
)
|
||||
: this._params.urlPath === "lovelace"
|
||||
: isLovelaceDashboard
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_default"
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_lovelace"
|
||||
)
|
||||
: html`
|
||||
<ha-form
|
||||
@@ -119,24 +116,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._toggleDefault}
|
||||
.disabled=${this._params.urlPath === "lovelace" &&
|
||||
defaultPanelUrlPath === "lovelace"}
|
||||
>
|
||||
${this._params.urlPath === defaultPanelUrlPath
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.remove_default"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default"
|
||||
)}
|
||||
</ha-button>
|
||||
: nothing}
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateDashboard}
|
||||
@@ -254,40 +236,6 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private async _toggleDefault() {
|
||||
const urlPath = this._params?.urlPath;
|
||||
if (!urlPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
// Add warning dialog to saying that this will change the default dashboard for all users
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_title"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_text"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
default_panel: urlPath === defaultPanel ? undefined : urlPath,
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
if (this._params?.urlPath && !this._params.dashboard?.id) {
|
||||
this.closeDialog();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiCheckCircleOutline,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiHomeCircleOutline,
|
||||
mdiHomeEdit,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
@@ -10,7 +11,6 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
@@ -29,6 +29,7 @@ import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||
import type { LovelacePanelConfig } from "../../../../data/lovelace";
|
||||
import type { LovelaceRawConfig } from "../../../../data/lovelace/config/types";
|
||||
import {
|
||||
@@ -45,7 +46,11 @@ import {
|
||||
fetchDashboards,
|
||||
updateDashboard,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import {
|
||||
DEFAULT_PANEL,
|
||||
getPanelIcon,
|
||||
getPanelTitle,
|
||||
} from "../../../../data/panel";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
@@ -56,12 +61,21 @@ import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
export const PANEL_DASHBOARDS = [
|
||||
"home",
|
||||
"light",
|
||||
"security",
|
||||
"climate",
|
||||
"energy",
|
||||
] as string[];
|
||||
|
||||
type DataTableItem = Pick<
|
||||
LovelaceDashboard,
|
||||
"icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path"
|
||||
> & {
|
||||
default: boolean;
|
||||
filename: string;
|
||||
localized_type: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
@@ -112,7 +126,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeGrouping?: string = "type";
|
||||
private _activeGrouping?: string = "localized_type";
|
||||
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-collapsed",
|
||||
@@ -167,7 +181,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
<ha-svg-icon
|
||||
.id="default-icon-${dashboard.title}"
|
||||
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
|
||||
.path=${mdiCheckCircleOutline}
|
||||
.path=${mdiHomeCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
.for="default-icon-${dashboard.title}"
|
||||
@@ -183,7 +197,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
},
|
||||
};
|
||||
|
||||
columns.type = {
|
||||
columns.localized_type = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.type"
|
||||
),
|
||||
@@ -253,7 +267,15 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
.items=${[
|
||||
...(this._canEdit(dashboard.url_path)
|
||||
{
|
||||
path: mdiHomeEdit,
|
||||
label: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.set_as_default"
|
||||
),
|
||||
action: () => this._handleSetAsDefault(dashboard),
|
||||
disabled: dashboard.default,
|
||||
},
|
||||
...(dashboard.type === "user_created"
|
||||
? [
|
||||
{
|
||||
path: mdiPencil,
|
||||
@@ -262,10 +284,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
),
|
||||
action: () => this._handleEdit(dashboard),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(this._canDelete(dashboard.url_path)
|
||||
? [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.delete"
|
||||
@@ -288,92 +306,43 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
private _getItems = memoize(
|
||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
|
||||
const defaultMode = (
|
||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||
).mode;
|
||||
const mode = (this.hass.panels?.lovelace?.config as LovelacePanelConfig)
|
||||
.mode;
|
||||
const isDefault = defaultUrlPath === "lovelace";
|
||||
const result: DataTableItem[] = [
|
||||
{
|
||||
icon: "mdi:view-dashboard",
|
||||
title: this.hass.localize("panel.states"),
|
||||
default: isDefault,
|
||||
show_in_sidebar: isDefault,
|
||||
show_in_sidebar: true,
|
||||
require_admin: false,
|
||||
url_path: "lovelace",
|
||||
mode: defaultMode,
|
||||
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||
type: this._localizeType("built_in"),
|
||||
mode: mode,
|
||||
filename: mode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||
type: "built_in",
|
||||
localized_type: this._localizeType("built_in"),
|
||||
},
|
||||
];
|
||||
if (isComponentLoaded(this.hass, "energy")) {
|
||||
result.push({
|
||||
icon: "mdi:lightning-bolt",
|
||||
title: this.hass.localize(`ui.panel.config.dashboard.energy.main`),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "energy",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.light) {
|
||||
result.push({
|
||||
icon: this.hass.panels.light.icon || "mdi:lamps",
|
||||
title: this.hass.localize("panel.light"),
|
||||
PANEL_DASHBOARDS.forEach((panel) => {
|
||||
const panelInfo = this.hass.panels[panel];
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const item: DataTableItem = {
|
||||
icon: getPanelIcon(panelInfo),
|
||||
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "light",
|
||||
url_path: panelInfo.url_path,
|
||||
filename: "",
|
||||
default: false,
|
||||
default: defaultUrlPath === panelInfo.url_path,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
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(
|
||||
...dashboards
|
||||
@@ -386,7 +355,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
filename: "",
|
||||
...dashboard,
|
||||
default: defaultUrlPath === dashboard.url_path,
|
||||
type: this._localizeType("user_created"),
|
||||
type: "user_created",
|
||||
localized_type: this._localizeType("user_created"),
|
||||
}) satisfies DataTableItem
|
||||
)
|
||||
);
|
||||
@@ -486,21 +456,33 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._openDetailDialog(dashboard, urlPath);
|
||||
}
|
||||
|
||||
private _canDelete(urlPath: string) {
|
||||
return ![
|
||||
"lovelace",
|
||||
"energy",
|
||||
"light",
|
||||
"security",
|
||||
"climate",
|
||||
"home",
|
||||
].includes(urlPath);
|
||||
private _handleSetAsDefault = async (item: DataTableItem) => {
|
||||
if (item.default) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _canEdit(urlPath: string) {
|
||||
return !["light", "security", "climate", "home"].includes(urlPath);
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
default_panel: item.url_path,
|
||||
});
|
||||
};
|
||||
|
||||
private _handleDelete = async (item: DataTableItem) => {
|
||||
const dashboard = this._dashboards.find(
|
||||
(res) => res.url_path === item.url_path
|
||||
@@ -581,10 +563,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
private async _deleteDashboard(
|
||||
dashboard: LovelaceDashboard
|
||||
): Promise<boolean> {
|
||||
if (!this._canDelete(dashboard.url_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.confirm_delete_title",
|
||||
|
||||
@@ -271,7 +271,6 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.scene.picker.headers.area"),
|
||||
defaultHidden: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
|
||||
@@ -281,7 +281,6 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
|
||||
},
|
||||
area: {
|
||||
title: localize("ui.panel.config.script.picker.headers.area"),
|
||||
defaultHidden: true,
|
||||
groupable: true,
|
||||
filterable: true,
|
||||
sortable: true,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time";
|
||||
import type {
|
||||
PipelineRunEvent,
|
||||
@@ -20,6 +21,8 @@ import "../../../../layouts/hass-subpage";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant, Route } from "../../../../types";
|
||||
import "./assist-render-pipeline-events";
|
||||
import type { ChatLog } from "../../../../data/chat_log";
|
||||
import { subscribeChatLog } from "../../../../data/chat_log";
|
||||
|
||||
@customElement("assist-pipeline-debug")
|
||||
export class AssistPipelineDebug extends LitElement {
|
||||
@@ -37,8 +40,12 @@ export class AssistPipelineDebug extends LitElement {
|
||||
|
||||
@state() private _events?: PipelineRunEvent[];
|
||||
|
||||
@state() private _chatLog?: ChatLog;
|
||||
|
||||
private _unsubRefreshEventsID?: number;
|
||||
|
||||
private _unsubChatLogUpdates?: Promise<UnsubscribeFunc>;
|
||||
|
||||
protected render() {
|
||||
return html`<hass-subpage
|
||||
.narrow=${this.narrow}
|
||||
@@ -106,6 +113,7 @@ export class AssistPipelineDebug extends LitElement {
|
||||
? html`<assist-render-pipeline-events
|
||||
.hass=${this.hass}
|
||||
.events=${this._events}
|
||||
.chatLog=${this._chatLog}
|
||||
></assist-render-pipeline-events>`
|
||||
: ""}
|
||||
</div>
|
||||
@@ -120,6 +128,10 @@ export class AssistPipelineDebug extends LitElement {
|
||||
clearRefresh = true;
|
||||
}
|
||||
if (changedProperties.has("_runId")) {
|
||||
if (this._unsubChatLogUpdates) {
|
||||
this._unsubChatLogUpdates.then((unsub) => unsub());
|
||||
this._unsubChatLogUpdates = undefined;
|
||||
}
|
||||
this._fetchEvents();
|
||||
clearRefresh = true;
|
||||
}
|
||||
@@ -135,6 +147,10 @@ export class AssistPipelineDebug extends LitElement {
|
||||
clearTimeout(this._unsubRefreshEventsID);
|
||||
this._unsubRefreshEventsID = undefined;
|
||||
}
|
||||
if (this._unsubChatLogUpdates) {
|
||||
this._unsubChatLogUpdates.then((unsub) => unsub());
|
||||
this._unsubChatLogUpdates = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private async _fetchRuns() {
|
||||
@@ -185,8 +201,27 @@ export class AssistPipelineDebug extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!this._events!.length) {
|
||||
return;
|
||||
}
|
||||
if (!this._unsubChatLogUpdates && this._events[0].type === "run-start") {
|
||||
this._unsubChatLogUpdates = subscribeChatLog(
|
||||
this.hass,
|
||||
this._events[0].data.conversation_id,
|
||||
(chatLog) => {
|
||||
if (chatLog) {
|
||||
this._chatLog = chatLog;
|
||||
} else {
|
||||
this._unsubChatLogUpdates?.then((unsub) => unsub());
|
||||
this._unsubChatLogUpdates = undefined;
|
||||
}
|
||||
}
|
||||
);
|
||||
this._unsubChatLogUpdates.catch(() => {
|
||||
this._unsubChatLogUpdates = undefined;
|
||||
});
|
||||
}
|
||||
if (
|
||||
this._events?.length &&
|
||||
// If the last event is not a finish run event, the run is still ongoing.
|
||||
// Refresh events automatically.
|
||||
!["run-end", "error"].includes(this._events[this._events.length - 1].type)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import { extractSearchParam } from "../../../../common/url/search-params";
|
||||
import "../../../../components/ha-assist-pipeline-picker";
|
||||
import "../../../../components/ha-button";
|
||||
@@ -24,6 +25,8 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import { AudioRecorder } from "../../../../util/audio-recorder";
|
||||
import { fileDownload } from "../../../../util/file_download";
|
||||
import "./assist-render-pipeline-run";
|
||||
import type { ChatLog } from "../../../../data/chat_log";
|
||||
import { subscribeChatLog } from "../../../../data/chat_log";
|
||||
|
||||
@customElement("assist-pipeline-run-debug")
|
||||
export class AssistPipelineRunDebug extends LitElement {
|
||||
@@ -46,6 +49,13 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
@state() private _pipelineId?: string =
|
||||
extractSearchParam("pipeline") || undefined;
|
||||
|
||||
@state() private _chatLog?: ChatLog;
|
||||
|
||||
private _chatLogSubscription: {
|
||||
conversationId: string;
|
||||
unsub: Promise<UnsubscribeFunc>;
|
||||
} | null = null;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<hass-subpage
|
||||
@@ -178,6 +188,7 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
<assist-render-pipeline-run
|
||||
.hass=${this.hass}
|
||||
.pipelineRun=${run}
|
||||
.chatLog=${this._chatLog}
|
||||
></assist-render-pipeline-run>
|
||||
`
|
||||
)}
|
||||
@@ -186,6 +197,14 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
public disconnectedCallback(): void {
|
||||
super.disconnectedCallback();
|
||||
if (this._chatLogSubscription) {
|
||||
this._chatLogSubscription.unsub.then((unsub) => unsub());
|
||||
this._chatLogSubscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
private get conversationId(): string | null {
|
||||
return this._pipelineRuns.length === 0
|
||||
? null
|
||||
@@ -408,6 +427,32 @@ export class AssistPipelineRunDebug extends LitElement {
|
||||
added = true;
|
||||
}
|
||||
callback(updatedRun);
|
||||
|
||||
const conversationId = this.conversationId;
|
||||
if (
|
||||
!this._chatLog &&
|
||||
conversationId &&
|
||||
(!this._chatLogSubscription ||
|
||||
this._chatLogSubscription.conversationId !== conversationId)
|
||||
) {
|
||||
if (this._chatLogSubscription) {
|
||||
this._chatLogSubscription.unsub.then((unsub) => unsub());
|
||||
}
|
||||
this._chatLogSubscription = {
|
||||
conversationId,
|
||||
unsub: subscribeChatLog(this.hass, conversationId, (chatLog) => {
|
||||
if (chatLog) {
|
||||
this._chatLog = chatLog;
|
||||
} else {
|
||||
this._chatLogSubscription?.unsub.then((unsub) => unsub());
|
||||
this._chatLogSubscription = null;
|
||||
}
|
||||
}),
|
||||
};
|
||||
this._chatLogSubscription.unsub.catch(() => {
|
||||
this._chatLogSubscription = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
...options,
|
||||
|
||||
@@ -9,6 +9,7 @@ import type {
|
||||
import { processEvent } from "../../../../data/assist_pipeline";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "./assist-render-pipeline-run";
|
||||
import type { ChatLog } from "../../../../data/chat_log";
|
||||
|
||||
@customElement("assist-render-pipeline-events")
|
||||
export class AssistPipelineEvents extends LitElement {
|
||||
@@ -16,6 +17,8 @@ export class AssistPipelineEvents extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public events!: PipelineRunEvent[];
|
||||
|
||||
@property({ attribute: false }) public chatLog?: ChatLog;
|
||||
|
||||
private _processEvents = memoizeOne(
|
||||
(events: PipelineRunEvent[]): PipelineRun | undefined => {
|
||||
let run: PipelineRun | undefined;
|
||||
@@ -56,6 +59,7 @@ export class AssistPipelineEvents extends LitElement {
|
||||
<assist-render-pipeline-run
|
||||
.hass=${this.hass}
|
||||
.pipelineRun=${run}
|
||||
.chatLog=${this.chatLog}
|
||||
></assist-render-pipeline-run>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-alert";
|
||||
@@ -12,6 +12,12 @@ import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/ha-yaml-editor";
|
||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import type { LocalizeKeys } from "../../../../common/translations/localize";
|
||||
import type {
|
||||
ChatLogAssistantContent,
|
||||
ChatLog,
|
||||
ChatLogContent,
|
||||
ChatLogUserContent,
|
||||
} from "../../../../data/chat_log";
|
||||
|
||||
const RUN_DATA = ["pipeline", "language"];
|
||||
const WAKE_WORD_DATA = ["engine"];
|
||||
@@ -119,7 +125,7 @@ const dataMinusKeysRender = (
|
||||
result[key] = data[key];
|
||||
}
|
||||
return render
|
||||
? html`<ha-expansion-panel>
|
||||
? html`<ha-expansion-panel class="yaml-expansion">
|
||||
<span slot="header"
|
||||
>${hass.localize("ui.panel.config.voice_assistants.debug.raw")}</span
|
||||
>
|
||||
@@ -134,6 +140,8 @@ export class AssistPipelineDebug extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public pipelineRun!: PipelineRun;
|
||||
|
||||
@property({ attribute: false }) public chatLog?: ChatLog;
|
||||
|
||||
private _audioElement?: HTMLAudioElement;
|
||||
|
||||
private get _isPlaying(): boolean {
|
||||
@@ -147,8 +155,23 @@ export class AssistPipelineDebug extends LitElement {
|
||||
) || "ready"
|
||||
: "ready";
|
||||
|
||||
const messages: { from: string; text: string }[] = [];
|
||||
let messages: ChatLogContent[];
|
||||
|
||||
if (this.chatLog) {
|
||||
messages = this.chatLog.content.filter(
|
||||
this.pipelineRun.finished
|
||||
? (content: ChatLogContent) =>
|
||||
content.role === "system" ||
|
||||
(content.created >= this.pipelineRun.started &&
|
||||
content.created <= this.pipelineRun.finished!)
|
||||
: (content: ChatLogContent) =>
|
||||
content.role === "system" ||
|
||||
content.created >= this.pipelineRun.started
|
||||
);
|
||||
} else {
|
||||
messages = [];
|
||||
|
||||
// We don't have the chat log everywhere yet, just fallback for now.
|
||||
const userMessage =
|
||||
(this.pipelineRun.init_options &&
|
||||
"text" in this.pipelineRun.init_options.input
|
||||
@@ -159,19 +182,20 @@ export class AssistPipelineDebug extends LitElement {
|
||||
|
||||
if (userMessage) {
|
||||
messages.push({
|
||||
from: "user",
|
||||
text: userMessage,
|
||||
});
|
||||
role: "user",
|
||||
content: userMessage,
|
||||
} as ChatLogUserContent);
|
||||
}
|
||||
|
||||
if (
|
||||
this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech
|
||||
) {
|
||||
messages.push({
|
||||
from: "hass",
|
||||
text: this.pipelineRun.intent.intent_output.response.speech.plain
|
||||
.speech,
|
||||
});
|
||||
role: "assistant",
|
||||
content:
|
||||
this.pipelineRun.intent.intent_output.response.speech.plain.speech,
|
||||
} as ChatLogAssistantContent);
|
||||
}
|
||||
}
|
||||
|
||||
return html`
|
||||
@@ -190,9 +214,57 @@ export class AssistPipelineDebug extends LitElement {
|
||||
${messages.length > 0
|
||||
? html`
|
||||
<div class="messages">
|
||||
${messages.map(
|
||||
({ from, text }) => html`
|
||||
<div class=${`message ${from}`}>${text}</div>
|
||||
${messages.map((content) =>
|
||||
content.role === "system" || content.role === "tool_result"
|
||||
? html`
|
||||
<ha-expansion-panel
|
||||
class="content-expansion ${content.role}"
|
||||
>
|
||||
<div slot="header">
|
||||
${content.role === "system"
|
||||
? "System"
|
||||
: `Result for ${content.tool_name}`}
|
||||
</div>
|
||||
${content.role === "system"
|
||||
? html`<pre>${content.content}</pre>`
|
||||
: html`
|
||||
<ha-yaml-editor
|
||||
read-only
|
||||
auto-update
|
||||
.value=${content}
|
||||
></ha-yaml-editor>
|
||||
`}
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: html`
|
||||
${content.content
|
||||
? html`
|
||||
<div class=${`message ${content.role}`}>
|
||||
${content.content}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${content.role === "assistant" &&
|
||||
content.tool_calls?.length
|
||||
? html`
|
||||
<ha-expansion-panel
|
||||
class="content-expansion assistant"
|
||||
>
|
||||
<span slot="header">
|
||||
Call
|
||||
${content.tool_calls.length === 1
|
||||
? content.tool_calls[0].tool_name
|
||||
: `${content.tool_calls.length} tools`}
|
||||
</span>
|
||||
|
||||
<ha-yaml-editor
|
||||
read-only
|
||||
auto-update
|
||||
.value=${content.tool_calls}
|
||||
></ha-yaml-editor>
|
||||
</ha-expansion-panel>
|
||||
`
|
||||
: nothing}
|
||||
`
|
||||
)}
|
||||
</div>
|
||||
@@ -442,7 +514,7 @@ export class AssistPipelineDebug extends LitElement {
|
||||
: ""}
|
||||
${maybeRenderError(this.pipelineRun, "tts", lastRunStage)}
|
||||
<ha-card>
|
||||
<ha-expansion-panel>
|
||||
<ha-expansion-panel class="yaml-expansion">
|
||||
<span slot="header"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.voice_assistants.debug.raw"
|
||||
@@ -519,12 +591,12 @@ export class AssistPipelineDebug extends LitElement {
|
||||
.row > div:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
ha-expansion-panel {
|
||||
.yaml-expansion {
|
||||
padding-left: 8px;
|
||||
padding-inline-start: 8px;
|
||||
padding-inline-end: initial;
|
||||
}
|
||||
.card-content ha-expansion-panel {
|
||||
.card-content .yaml-expansion {
|
||||
padding-left: 0px;
|
||||
padding-inline-start: 0px;
|
||||
padding-inline-end: initial;
|
||||
@@ -540,27 +612,59 @@ export class AssistPipelineDebug extends LitElement {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.content-expansion {
|
||||
margin: 8px 0;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
clear: both;
|
||||
padding: 0 8px;
|
||||
--input-fill-color: none;
|
||||
max-width: calc(100% - 24px);
|
||||
--expansion-panel-summary-padding: 0px;
|
||||
--expansion-panel-content-padding: 0px;
|
||||
}
|
||||
|
||||
.content-expansion *[slot="header"] {
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
.system {
|
||||
background-color: var(--success-color);
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.message,
|
||||
.content-expansion {
|
||||
font-size: var(--ha-font-size-l);
|
||||
margin: 8px 0;
|
||||
padding: 8px;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.message.user {
|
||||
.messages pre {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.user,
|
||||
.tool_result {
|
||||
margin-left: 24px;
|
||||
margin-inline-start: 24px;
|
||||
margin-inline-end: initial;
|
||||
float: var(--float-end);
|
||||
text-align: right;
|
||||
border-bottom-right-radius: 0px;
|
||||
background-color: var(--light-primary-color);
|
||||
color: var(--text-light-primary-color, var(--primary-text-color));
|
||||
direction: var(--direction);
|
||||
}
|
||||
|
||||
.message.hass {
|
||||
.message.user,
|
||||
.content-expansion div[slot="header"] {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.assistant {
|
||||
margin-right: 24px;
|
||||
margin-inline-end: 24px;
|
||||
margin-inline-start: initial;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { getEnergyColor } from "./common/color";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/ha-card";
|
||||
import "./common/hui-energy-graph-chip";
|
||||
import type {
|
||||
EnergyData,
|
||||
EnergySumData,
|
||||
@@ -67,6 +68,8 @@ export class HuiEnergyUsageGraphCard
|
||||
|
||||
@state() private _compareEnd?: Date;
|
||||
|
||||
@state() private _total?: number;
|
||||
|
||||
protected hassSubscribeRequiredHostProps = ["_config"];
|
||||
|
||||
public hassSubscribe(): UnsubscribeFunc[] {
|
||||
@@ -100,9 +103,19 @@ export class HuiEnergyUsageGraphCard
|
||||
|
||||
return html`
|
||||
<ha-card>
|
||||
${this._config.title
|
||||
? html`<h1 class="card-header">${this._config.title}</h1>`
|
||||
: ""}
|
||||
<div class="card-header">
|
||||
<span>${this._config.title ? this._config.title : nothing}</span>
|
||||
${this._total
|
||||
? html`<hui-energy-graph-chip
|
||||
.tooltip=${this._formatTotal(this._total)}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.lovelace.cards.energy.energy_usage_graph.total_usage",
|
||||
{ num: formatNumber(this._total, this.hass.locale) }
|
||||
)}
|
||||
</hui-energy-graph-chip>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div
|
||||
class="content ${classMap({
|
||||
"has-header": !!this._config.title,
|
||||
@@ -338,6 +351,13 @@ export class HuiEnergyUsageGraphCard
|
||||
datasets.sort((a, b) => a.order - b.order);
|
||||
fillDataGapsAndRoundCaps(datasets);
|
||||
this._chartData = datasets;
|
||||
this._total = this._processTotal(consumption);
|
||||
}
|
||||
|
||||
private _processTotal(consumption: EnergyConsumptionData) {
|
||||
return consumption.total.used_total > 0
|
||||
? consumption.total.used_total
|
||||
: undefined;
|
||||
}
|
||||
|
||||
private _processDataSet(
|
||||
@@ -515,6 +535,9 @@ export class HuiEnergyUsageGraphCard
|
||||
height: 100%;
|
||||
}
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.content {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { Lovelace } from "../types";
|
||||
import { deleteBadge } from "./config-util";
|
||||
import type { LovelaceCardPath } from "./lovelace-path";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface DeleteBadgeParams {
|
||||
path: LovelaceCardPath;
|
||||
@@ -23,14 +24,13 @@ export async function performDeleteBadge(
|
||||
return;
|
||||
}
|
||||
|
||||
const action = async () => {
|
||||
lovelace.saveConfig(oldConfig);
|
||||
};
|
||||
|
||||
lovelace.showToast({
|
||||
message: hass.localize("ui.common.successfully_deleted"),
|
||||
duration: 8000,
|
||||
action: { action, text: hass.localize("ui.common.undo") },
|
||||
action: {
|
||||
action: () => fireEvent(window, "undo-change"),
|
||||
text: hass.localize("ui.common.undo"),
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { HomeAssistant } from "../../../types";
|
||||
import type { Lovelace } from "../types";
|
||||
import { deleteCard } from "./config-util";
|
||||
import type { LovelaceCardPath } from "./lovelace-path";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
|
||||
export interface DeleteCardParams {
|
||||
path: LovelaceCardPath;
|
||||
@@ -23,14 +24,13 @@ export async function performDeleteCard(
|
||||
return;
|
||||
}
|
||||
|
||||
const action = async () => {
|
||||
lovelace.saveConfig(oldConfig);
|
||||
};
|
||||
|
||||
lovelace.showToast({
|
||||
message: hass.localize("ui.common.successfully_deleted"),
|
||||
duration: 8000,
|
||||
action: { action, text: hass.localize("ui.common.undo") },
|
||||
action: {
|
||||
action: () => fireEvent(window, "undo-change"),
|
||||
text: hass.localize("ui.common.undo"),
|
||||
},
|
||||
});
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -11,10 +11,12 @@ import {
|
||||
mdiMagnify,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
mdiRedo,
|
||||
mdiRefresh,
|
||||
mdiRobot,
|
||||
mdiShape,
|
||||
mdiSofa,
|
||||
mdiUndo,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
@@ -50,7 +52,10 @@ import "../../components/ha-tab-group-tab";
|
||||
import "../../components/ha-tooltip";
|
||||
import { createAreaRegistryEntry } from "../../data/area_registry";
|
||||
import type { LovelacePanelConfig } from "../../data/lovelace";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import type {
|
||||
LovelaceConfig,
|
||||
LovelaceRawConfig,
|
||||
} from "../../data/lovelace/config/types";
|
||||
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
||||
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
||||
import {
|
||||
@@ -92,6 +97,7 @@ import "./views/hui-view";
|
||||
import type { HUIView } from "./views/hui-view";
|
||||
import "./views/hui-view-background";
|
||||
import "./views/hui-view-container";
|
||||
import { UndoRedoController } from "../../common/controllers/undo-redo-controller";
|
||||
|
||||
interface ActionItem {
|
||||
icon: string;
|
||||
@@ -113,6 +119,11 @@ interface SubActionItem {
|
||||
visible: boolean | undefined;
|
||||
}
|
||||
|
||||
interface UndoStackItem {
|
||||
location: string;
|
||||
config: LovelaceRawConfig;
|
||||
}
|
||||
|
||||
@customElement("hui-root")
|
||||
class HUIRoot extends LitElement {
|
||||
@property({ attribute: false }) public panel?: PanelInfo<LovelacePanelConfig>;
|
||||
@@ -130,12 +141,22 @@ class HUIRoot extends LitElement {
|
||||
|
||||
@state() private _curView?: number | "hass-unused-entities";
|
||||
|
||||
private _configChangedByUndo = false;
|
||||
|
||||
private _viewCache?: Record<string, HUIView>;
|
||||
|
||||
private _viewScrollPositions: Record<string, number> = {};
|
||||
|
||||
private _restoreScroll = false;
|
||||
|
||||
private _undoRedoController = new UndoRedoController<UndoStackItem>(this, {
|
||||
apply: (config) => this._applyUndoRedo(config),
|
||||
currentConfig: () => ({
|
||||
location: this.route!.path,
|
||||
config: this.lovelace!.rawConfig,
|
||||
}),
|
||||
});
|
||||
|
||||
private _debouncedConfigChanged: () => void;
|
||||
|
||||
private _conversation = memoizeOne((_components) =>
|
||||
@@ -157,7 +178,29 @@ class HUIRoot extends LitElement {
|
||||
const result: TemplateResult[] = [];
|
||||
if (this._editMode) {
|
||||
result.push(
|
||||
html`<ha-button
|
||||
html`<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiUndo}
|
||||
@click=${this._undo}
|
||||
.disabled=${!this._undoRedoController.canUndo}
|
||||
id="button-undo"
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-tooltip placement="bottom" for="button-undo">
|
||||
${this.hass.localize("ui.common.undo")}
|
||||
</ha-tooltip>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiRedo}
|
||||
@click=${this._redo}
|
||||
.disabled=${!this._undoRedoController.canRedo}
|
||||
id="button-redo"
|
||||
>
|
||||
</ha-icon-button>
|
||||
<ha-tooltip placement="bottom" for="button-redo">
|
||||
${this.hass.localize("ui.common.redo")}
|
||||
</ha-tooltip>
|
||||
<ha-button
|
||||
appearance="filled"
|
||||
size="small"
|
||||
class="exit-edit-mode"
|
||||
@@ -645,6 +688,27 @@ class HUIRoot extends LitElement {
|
||||
window.history.scrollRestoration = "auto";
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues): void {
|
||||
if (changedProperties.has("lovelace")) {
|
||||
const oldLovelace = changedProperties.get("lovelace") as
|
||||
| Lovelace
|
||||
| undefined;
|
||||
|
||||
if (
|
||||
oldLovelace &&
|
||||
this.lovelace!.rawConfig !== oldLovelace!.rawConfig &&
|
||||
!this._configChangedByUndo
|
||||
) {
|
||||
this._undoRedoController.commit({
|
||||
location: this.route!.path,
|
||||
config: oldLovelace.rawConfig,
|
||||
});
|
||||
} else {
|
||||
this._configChangedByUndo = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues): void {
|
||||
super.updated(changedProperties);
|
||||
|
||||
@@ -1029,6 +1093,7 @@ class HUIRoot extends LitElement {
|
||||
|
||||
private _editModeDisable(): void {
|
||||
this.lovelace!.setEditMode(false);
|
||||
this._undoRedoController.reset();
|
||||
}
|
||||
|
||||
private async _editDashboard() {
|
||||
@@ -1207,6 +1272,36 @@ class HUIRoot extends LitElement {
|
||||
showShortcutsDialog(this);
|
||||
}
|
||||
|
||||
private async _applyUndoRedo(item: UndoStackItem) {
|
||||
this._configChangedByUndo = true;
|
||||
try {
|
||||
await this.lovelace!.saveConfig(item.config);
|
||||
} catch (err: any) {
|
||||
this._configChangedByUndo = false;
|
||||
showToast(this, {
|
||||
message: this.hass.localize(
|
||||
"ui.panel.lovelace.editor.undo_redo_failed_to_apply_changes",
|
||||
{
|
||||
error: err.message,
|
||||
}
|
||||
),
|
||||
duration: 4000,
|
||||
dismissable: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this._navigateToView(item.location);
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
this._undoRedoController.undo();
|
||||
}
|
||||
|
||||
private _redo() {
|
||||
this._undoRedoController.redo();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-divider";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-select";
|
||||
import "../../components/ha-settings-row";
|
||||
import { saveFrontendUserData } from "../../data/frontend";
|
||||
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { saveFrontendUserData } from "../../data/frontend";
|
||||
import { getPanelTitle } from "../../data/panel";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards";
|
||||
|
||||
const USE_SYSTEM_VALUE = "___use_system___";
|
||||
|
||||
@@ -47,12 +50,24 @@ class HaPickDashboardRow extends LitElement {
|
||||
<ha-list-item .value=${USE_SYSTEM_VALUE}>
|
||||
${this.hass.localize("ui.panel.profile.dashboard.system")}
|
||||
</ha-list-item>
|
||||
<ha-divider></ha-divider>
|
||||
<ha-list-item value="lovelace">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="home">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.home")}
|
||||
${PANEL_DASHBOARDS.map((panel) => {
|
||||
const panelInfo = this.hass.panels[panel] as
|
||||
| PanelInfo
|
||||
| undefined;
|
||||
if (!panelInfo) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-list-item value=${panelInfo.url_path}>
|
||||
${getPanelTitle(this.hass, panelInfo)}
|
||||
</ha-list-item>
|
||||
`;
|
||||
})}
|
||||
<ha-divider></ha-divider>
|
||||
${this._dashboards.map((dashboard) => {
|
||||
if (!this.hass.user!.is_admin && dashboard.require_admin) {
|
||||
return "";
|
||||
|
||||
@@ -20,6 +20,7 @@ export const colorStyles = css`
|
||||
--divider-color: rgba(0, 0, 0, 0.12);
|
||||
--outline-color: rgba(0, 0, 0, 0.12);
|
||||
--outline-hover-color: rgba(0, 0, 0, 0.24);
|
||||
--shadow-color: rgba(0, 0, 0, 0.16);
|
||||
|
||||
/* rgb */
|
||||
--rgb-primary-color: 0, 154, 199;
|
||||
@@ -224,7 +225,7 @@ export const colorStyles = css`
|
||||
--table-row-alternative-background-color: var(--secondary-background-color);
|
||||
--data-table-background-color: var(--card-background-color);
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--bar-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.16);
|
||||
--bar-box-shadow: 0 2px 12px var(--shadow-color);
|
||||
|
||||
/* https://github.com/material-components/material-web/blob/master/docs/theming.md */
|
||||
--mdc-theme-primary: var(--primary-color);
|
||||
@@ -307,6 +308,8 @@ export const darkColorStyles = css`
|
||||
--divider-color: rgba(225, 225, 225, 0.12);
|
||||
--outline-color: rgba(225, 225, 225, 0.12);
|
||||
--outline-hover-color: rgba(225, 225, 225, 0.24);
|
||||
--shadow-color: rgba(0, 0, 0, 0.48);
|
||||
|
||||
--mdc-ripple-color: #aaaaaa;
|
||||
--mdc-linear-progress-buffer-color: rgba(255, 255, 255, 0.1);
|
||||
|
||||
@@ -350,7 +353,7 @@ export const darkColorStyles = css`
|
||||
--ha-button-neutral-color: #d9dae0;
|
||||
--ha-button-neutral-light-color: #6a7081;
|
||||
|
||||
--bar-box-shadow: 0 2px 12px rgba(0, 0, 0, 0.48);
|
||||
--bar-box-shadow: 0 2px 12px var(--shadow-color);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -52,6 +52,8 @@ export const waColorStyles = css`
|
||||
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
|
||||
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
|
||||
|
||||
--wa-color-text-quiet: var(--ha-color-text-secondary);
|
||||
|
||||
--wa-color-text-normal: var(--ha-color-text-primary);
|
||||
--wa-color-surface-default: var(--card-background-color);
|
||||
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
|
||||
@@ -62,5 +64,7 @@ export const waColorStyles = css`
|
||||
|
||||
--wa-focus-ring-color: var(--ha-color-neutral-60);
|
||||
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
|
||||
|
||||
--wa-color-text-normal: var(--ha-color-text-primary);
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -9,12 +9,16 @@ export const waMainStyles = css`
|
||||
--wa-focus-ring-offset: 2px;
|
||||
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
|
||||
|
||||
--wa-space-xs: var(--ha-space-2);
|
||||
--wa-space-m: var(--ha-space-4);
|
||||
--wa-space-l: var(--ha-space-6);
|
||||
--wa-space-xl: var(--ha-space-8);
|
||||
|
||||
--wa-form-control-padding-block: 0.75em;
|
||||
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
|
||||
|
||||
--wa-font-weight-action: var(--ha-font-weight-medium);
|
||||
--wa-transition-normal: 150ms;
|
||||
--wa-transition-fast: 75ms;
|
||||
--wa-transition-easing: ease;
|
||||
|
||||
@@ -28,6 +32,7 @@ export const waMainStyles = css`
|
||||
|
||||
--wa-line-height-condensed: var(--ha-line-height-condensed);
|
||||
|
||||
--wa-font-size-m: var(--ha-font-size-m);
|
||||
--wa-shadow-s: var(--ha-box-shadow-s);
|
||||
--wa-shadow-m: var(--ha-box-shadow-m);
|
||||
--wa-shadow-l: var(--ha-box-shadow-l);
|
||||
|
||||
@@ -2217,7 +2217,9 @@
|
||||
"sidebar_toggle": "Sidebar toggle",
|
||||
"edit_sidebar": "Edit sidebar",
|
||||
"edit_subtitle": "Synced on all devices",
|
||||
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device."
|
||||
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device.",
|
||||
"reset_to_defaults": "Reset to defaults",
|
||||
"reset_confirmation": "Are you sure you want to reset the sidebar to its default configuration? This will restore the original order and visibility of all panels."
|
||||
},
|
||||
"panel": {
|
||||
"home": {
|
||||
@@ -3508,6 +3510,7 @@
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"add_dashboard": "Add dashboard",
|
||||
"set_as_default": "Set as default",
|
||||
"type": {
|
||||
"user_created": "User created",
|
||||
"built_in": "Built-in"
|
||||
@@ -3516,7 +3519,7 @@
|
||||
"confirm_delete_title": "Delete {dashboard_title}?",
|
||||
"confirm_delete_text": "This dashboard will be permanently deleted.",
|
||||
"cant_edit_yaml": "Dashboards created in YAML cannot be edited from the UI. Change them in configuration.yaml.",
|
||||
"cant_edit_default": "The default dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.",
|
||||
"cant_edit_lovelace": "The Overview dashboard title and icon cannot be changed. You can create a new dashboard to get more customization options.",
|
||||
"detail": {
|
||||
"edit_dashboard": "Edit dashboard",
|
||||
"new_dashboard": "Add new dashboard",
|
||||
@@ -3533,9 +3536,7 @@
|
||||
"set_default": "Set as default",
|
||||
"remove_default": "Remove as default",
|
||||
"set_default_confirm_title": "Set as default dashboard?",
|
||||
"set_default_confirm_text": "This will replace the current default dashboard. Users can still override their default dashboard in their profile settings.",
|
||||
"remove_default_confirm_title": "Remove default dashboard?",
|
||||
"remove_default_confirm_text": "The default dashboard will be changed to Overview for every user. Users can still override their default dashboard in their profile settings."
|
||||
"set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant. Each user can change this in their profile."
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
@@ -4009,7 +4010,25 @@
|
||||
"item_pasted": "{item} pasted",
|
||||
"ctrl": "Ctrl",
|
||||
"del": "Del",
|
||||
"targets": "Targets",
|
||||
"select_target": "Select a target",
|
||||
"home": "Home",
|
||||
"unassigned": "Unassigned",
|
||||
"blocks": "Blocks",
|
||||
"show_more": "Show more",
|
||||
"unassigned_entities": "Unassigned entities",
|
||||
"unassigned_devices": "Unassigned devices",
|
||||
"empty_section_search": {
|
||||
"block": "No blocks found for {term}",
|
||||
"entity": "No entities found for {term}",
|
||||
"device": "No devices found for {term}",
|
||||
"area": "No areas or floors found for {term}",
|
||||
"label": "No labels found for {term}"
|
||||
},
|
||||
"load_target_items_failed": "Failed to load target items for",
|
||||
"other_areas": "Other areas",
|
||||
"services": "Services",
|
||||
"helpers": "Helpers",
|
||||
"triggers": {
|
||||
"name": "Triggers",
|
||||
"header": "When",
|
||||
@@ -4017,7 +4036,10 @@
|
||||
"learn_more": "Learn more about triggers",
|
||||
"triggered": "Triggered",
|
||||
"add": "Add trigger",
|
||||
"empty_search": "No triggers found for {term}",
|
||||
"empty_search": {
|
||||
"global": "No triggers and targets found for {term}",
|
||||
"item": "No triggers found for {term}"
|
||||
},
|
||||
"id": "Trigger ID",
|
||||
"optional": "Optional",
|
||||
"edit_id": "Edit ID",
|
||||
@@ -4038,6 +4060,7 @@
|
||||
"copied_to_clipboard": "Trigger copied to clipboard",
|
||||
"cut_to_clipboard": "Trigger cut to clipboard",
|
||||
"select": "Select a trigger",
|
||||
"no_items_for_target": "No triggers available for",
|
||||
"groups": {
|
||||
"device": {
|
||||
"label": "Device"
|
||||
@@ -4279,7 +4302,10 @@
|
||||
"description": "All conditions added here need to be satisfied for the automation to run. A condition can be satisfied or not at any given time, for example: ''If {user} is home''. You can use building blocks to create more complex conditions.",
|
||||
"learn_more": "Learn more about conditions",
|
||||
"add": "Add condition",
|
||||
"empty_search": "No conditions and blocks found for {term}",
|
||||
"empty_search": {
|
||||
"global": "No conditions, blocks and targets found for {term}",
|
||||
"item": "No conditions found for {term}"
|
||||
},
|
||||
"add_building_block": "Add building block",
|
||||
"test": "Test",
|
||||
"testing_error": "Condition did not pass",
|
||||
@@ -4302,6 +4328,7 @@
|
||||
"copied_to_clipboard": "Condition copied to clipboard",
|
||||
"cut_to_clipboard": "Condition cut to clipboard",
|
||||
"select": "Select a condition",
|
||||
"no_items_for_target": "No conditions available for",
|
||||
"groups": {
|
||||
"device": {
|
||||
"label": "Device"
|
||||
@@ -4447,7 +4474,10 @@
|
||||
"description": "All actions added here will be performed in sequence when the automation runs. An action usually controls one of your areas, devices, or entities, for example: 'Turn on the lights'. You can use building blocks to create more complex sequences of actions.",
|
||||
"learn_more": "Learn more about actions",
|
||||
"add": "Add action",
|
||||
"empty_search": "No actions and blocks found for {term}",
|
||||
"empty_search": {
|
||||
"global": "No actions, blocks and targets found for {term}",
|
||||
"item": "No actions found for {term}"
|
||||
},
|
||||
"add_building_block": "Add building block",
|
||||
"invalid_action": "Invalid action",
|
||||
"run": "Run action",
|
||||
@@ -4472,6 +4502,7 @@
|
||||
"copied_to_clipboard": "Action copied to clipboard",
|
||||
"cut_to_clipboard": "Action cut to clipboard",
|
||||
"select": "Select an action",
|
||||
"no_items_for_target": "No actions available for",
|
||||
"groups": {
|
||||
"device_id": {
|
||||
"label": "Device"
|
||||
@@ -6780,6 +6811,7 @@
|
||||
},
|
||||
"analytics": {
|
||||
"caption": "Analytics",
|
||||
"header": "Home Assistant analytics",
|
||||
"description": "Learn how to share data to improve Home Assistant",
|
||||
"preferences": {
|
||||
"base": {
|
||||
@@ -6797,10 +6829,21 @@
|
||||
"diagnostics": {
|
||||
"title": "Diagnostics",
|
||||
"description": "Share crash reports when unexpected errors occur."
|
||||
},
|
||||
"snapshots": {
|
||||
"title": "Devices",
|
||||
"description": "Generic information about your devices.",
|
||||
"header": "Device analytics",
|
||||
"info": "Anonymously share data about your devices to help build the Open Home Foundation’s device database. This free, open source resource helps users find useful information about smart home devices. Only device-specific details (like model or manufacturer) are shared — never personally identifying information (like the names you assign).",
|
||||
"learn_more": "Learn more about the device database and how we process your data",
|
||||
"alert": {
|
||||
"title": "Important",
|
||||
"content": "Only enable this option if you understand that your device information will be shared."
|
||||
}
|
||||
}
|
||||
},
|
||||
"need_base_enabled": "You need to enable basic analytics for this option to be available",
|
||||
"learn_more": "How we process your data",
|
||||
"learn_more": "Learn how we process your data",
|
||||
"intro": "Share anonymized information from your installation to help make Home Assistant better and help us convince manufacturers to add local control and privacy-focused features.",
|
||||
"download_device_info": "Preview device analytics"
|
||||
},
|
||||
@@ -7146,6 +7189,7 @@
|
||||
"energy_usage_graph": {
|
||||
"total_consumed": "Total consumed {num} kWh",
|
||||
"total_returned": "Total returned {num} kWh",
|
||||
"total_usage": "{num} kWh used",
|
||||
"combined_from_grid": "Combined from grid",
|
||||
"consumed_solar": "Consumed solar",
|
||||
"consumed_battery": "Consumed battery"
|
||||
@@ -7290,6 +7334,7 @@
|
||||
"editor": {
|
||||
"header": "Edit UI",
|
||||
"yaml_unsupported": "The edit UI is not available when in YAML mode.",
|
||||
"undo_redo_failed_to_apply_changes": "Unable to apply changes: {error}",
|
||||
"menu": {
|
||||
"open": "Open dashboard menu",
|
||||
"raw_editor": "Raw configuration editor",
|
||||
|
||||
20
yarn.lock
20
yarn.lock
@@ -1940,9 +1940,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@home-assistant/webawesome@npm:3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@home-assistant/webawesome@npm:3.0.0"
|
||||
"@home-assistant/webawesome@npm:3.0.0-ha.0":
|
||||
version: 3.0.0-ha.0
|
||||
resolution: "@home-assistant/webawesome@npm:3.0.0-ha.0"
|
||||
dependencies:
|
||||
"@ctrl/tinycolor": "npm:4.1.0"
|
||||
"@floating-ui/dom": "npm:^1.6.13"
|
||||
@@ -1953,7 +1953,7 @@ __metadata:
|
||||
lit: "npm:^3.2.1"
|
||||
nanoid: "npm:^5.1.5"
|
||||
qr-creator: "npm:^1.0.0"
|
||||
checksum: 10/03400894cfee8548fd5b1f5c56d31d253830e704b18ba69d36ce6b761d8b1bef2fb52cffba8d9b033033bb582f2f51a2d6444d82622f66d70150e2104fcb49e2
|
||||
checksum: 10/2034d498d5b26bb0573ebc2c9aadd144604bb48c04becbae0c67b16857d8e5d6562626e795974362c3fc41e9b593a9005595d8b5ff434b1569b2d724af13043b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -9226,7 +9226,7 @@ __metadata:
|
||||
"@fullcalendar/list": "npm:6.1.19"
|
||||
"@fullcalendar/luxon3": "npm:6.1.19"
|
||||
"@fullcalendar/timegrid": "npm:6.1.19"
|
||||
"@home-assistant/webawesome": "npm:3.0.0"
|
||||
"@home-assistant/webawesome": "npm:3.0.0-ha.0"
|
||||
"@lezer/highlight": "npm:1.2.3"
|
||||
"@lit-labs/motion": "npm:1.0.9"
|
||||
"@lit-labs/observers": "npm:2.0.6"
|
||||
@@ -9332,7 +9332,7 @@ __metadata:
|
||||
gulp-rename: "npm:2.1.0"
|
||||
gulp-zopfli-green: "npm:6.0.2"
|
||||
hls.js: "npm:1.6.14"
|
||||
home-assistant-js-websocket: "npm:9.5.0"
|
||||
home-assistant-js-websocket: "npm:9.6.0"
|
||||
html-minifier-terser: "npm:7.2.0"
|
||||
husky: "npm:9.1.7"
|
||||
idb-keyval: "npm:6.2.2"
|
||||
@@ -9393,10 +9393,10 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"home-assistant-js-websocket@npm:9.5.0":
|
||||
version: 9.5.0
|
||||
resolution: "home-assistant-js-websocket@npm:9.5.0"
|
||||
checksum: 10/42f991b3b85aa61be28984f099a001ac083fb3da54b2777283d0c97976c564a303d8d4ba467e1b8e29cbc33151cd6eef64c1a7d3392d62bbb9cbb27aa7ca9942
|
||||
"home-assistant-js-websocket@npm:9.6.0":
|
||||
version: 9.6.0
|
||||
resolution: "home-assistant-js-websocket@npm:9.6.0"
|
||||
checksum: 10/0eded7864632b5e19e92289ffac0e24308b1e8f425e292ae87ed21450852f7705db521e202614b1d5bbdb7948633143dce2524ed548db0c38486b40ed1ffa474
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user