mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-26 03:07:21 +00:00
Compare commits
12 Commits
encryption
...
fix_panel_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd1d63734a | ||
|
|
6dac37c535 | ||
|
|
14e1bdb586 | ||
|
|
fe50c1212a | ||
|
|
c01fbf5f47 | ||
|
|
5c8da28b61 | ||
|
|
bbb3c0208b | ||
|
|
82a3db39fe | ||
|
|
254857b53f | ||
|
|
1648be6b83 | ||
|
|
c4c774c217 | ||
|
|
6a0aab2088 |
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;
|
||||
}
|
||||
}
|
||||
@@ -66,7 +66,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
.path=${item.path}
|
||||
></ha-svg-icon>
|
||||
${item.label}
|
||||
</ha-md-menu-item> `
|
||||
</ha-md-menu-item>`
|
||||
)}
|
||||
</ha-md-button-menu>`
|
||||
: html`
|
||||
@@ -103,6 +103,7 @@ export class HaIconOverflowMenu extends LitElement {
|
||||
:host {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
cursor: initial;
|
||||
}
|
||||
div[role="separator"] {
|
||||
border-right: 1px solid var(--divider-color);
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface DisplayItem {
|
||||
label: string;
|
||||
description?: string;
|
||||
disableSorting?: boolean;
|
||||
disableHiding?: boolean;
|
||||
}
|
||||
|
||||
export interface DisplayValue {
|
||||
@@ -101,6 +102,7 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
icon,
|
||||
iconPath,
|
||||
disableSorting,
|
||||
disableHiding,
|
||||
} = item;
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@@ -155,18 +157,21 @@ export class HaItemDisplayEditor extends LitElement {
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-icon-button
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="end"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
||||
{
|
||||
label: label,
|
||||
}
|
||||
)}
|
||||
.value=${value}
|
||||
@click=${this._toggle}
|
||||
></ha-icon-button>
|
||||
${!isVisible || !disableHiding
|
||||
? html`<ha-icon-button
|
||||
.path=${isVisible ? mdiEye : mdiEyeOff}
|
||||
slot="end"
|
||||
.label=${this.hass.localize(
|
||||
`ui.components.items-display-editor.${isVisible ? "hide" : "show"}`,
|
||||
{
|
||||
label: label,
|
||||
}
|
||||
)}
|
||||
.value=${value}
|
||||
@click=${this._toggle}
|
||||
.disabled=${disableHiding || false}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
${isVisible && !disableSorting
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
|
||||
@@ -36,6 +36,11 @@ export class HaMdMenuItem extends MenuItemEl {
|
||||
::slotted([slot="headline"]) {
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
:host([disabled]) {
|
||||
opacity: 1;
|
||||
--md-menu-item-label-text-color: var(--disabled-text-color);
|
||||
--md-menu-item-leading-icon-color: var(--disabled-text-color);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
||||
import { titleCase } from "../common/string/title-case";
|
||||
import { fetchConfig } from "../data/lovelace/config/types";
|
||||
import type { LovelaceViewRawConfig } from "../data/lovelace/config/view";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import { getPanelIcon, getPanelTitle } from "../data/panel";
|
||||
import type { HomeAssistant, PanelInfo, ValueChangedEvent } from "../types";
|
||||
import "./ha-combo-box";
|
||||
import type { HaComboBox } from "./ha-combo-box";
|
||||
@@ -43,13 +43,8 @@ const createViewNavigationItem = (
|
||||
|
||||
const createPanelNavigationItem = (hass: HomeAssistant, panel: PanelInfo) => ({
|
||||
path: `/${panel.url_path}`,
|
||||
icon: panel.icon ?? "mdi:view-dashboard",
|
||||
title:
|
||||
panel.url_path === getDefaultPanelUrlPath(hass)
|
||||
? hass.localize("panel.states")
|
||||
: hass.localize(`panel.${panel.title}`) ||
|
||||
panel.title ||
|
||||
(panel.url_path ? titleCase(panel.url_path) : ""),
|
||||
icon: getPanelIcon(panel) || "mdi:view-dashboard",
|
||||
title: getPanelTitle(hass, panel) || "",
|
||||
});
|
||||
|
||||
@customElement("ha-navigation-picker")
|
||||
|
||||
@@ -1,22 +1,13 @@
|
||||
import {
|
||||
mdiBell,
|
||||
mdiCalendar,
|
||||
mdiCellphoneCog,
|
||||
mdiChartBox,
|
||||
mdiClipboardList,
|
||||
mdiCog,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiLightningBolt,
|
||||
mdiMenu,
|
||||
mdiMenuOpen,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiTooltipAccount,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import {
|
||||
customElement,
|
||||
eventOptions,
|
||||
@@ -33,7 +24,14 @@ import { computeRTL } from "../common/util/compute_rtl";
|
||||
import { throttle } from "../common/util/throttle";
|
||||
import { subscribeFrontendUserData } from "../data/frontend";
|
||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
|
||||
import { getDefaultPanelUrlPath } from "../data/panel";
|
||||
import {
|
||||
FIXED_PANELS,
|
||||
getDefaultPanelUrlPath,
|
||||
getPanelIcon,
|
||||
getPanelIconPath,
|
||||
getPanelTitle,
|
||||
SHOW_AFTER_SPACER_PANELS,
|
||||
} from "../data/panel";
|
||||
import type { PersistentNotification } from "../data/persistent_notification";
|
||||
import { subscribeNotifications } from "../data/persistent_notification";
|
||||
import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
@@ -54,8 +52,6 @@ import "./ha-spinner";
|
||||
import "./ha-svg-icon";
|
||||
import "./user/ha-user-badge";
|
||||
|
||||
const SHOW_AFTER_SPACER = ["config", "developer-tools"];
|
||||
|
||||
const SUPPORT_SCROLL_IF_NEEDED = "scrollIntoViewIfNeeded" in document.body;
|
||||
|
||||
const SORT_VALUE_URL_PATHS = {
|
||||
@@ -67,18 +63,6 @@ const SORT_VALUE_URL_PATHS = {
|
||||
config: 11,
|
||||
};
|
||||
|
||||
export const PANEL_ICONS = {
|
||||
calendar: mdiCalendar,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
lovelace: mdiViewDashboard,
|
||||
map: mdiTooltipAccount,
|
||||
"media-browser": mdiPlayBoxMultiple,
|
||||
todo: mdiClipboardList,
|
||||
};
|
||||
|
||||
const panelSorter = (
|
||||
reverseSort: string[],
|
||||
defaultPanel: string,
|
||||
@@ -155,16 +139,23 @@ export const computePanels = memoizeOne(
|
||||
const beforeSpacer: PanelInfo[] = [];
|
||||
const afterSpacer: PanelInfo[] = [];
|
||||
|
||||
Object.values(panels).forEach((panel) => {
|
||||
const allPanels = Object.values(panels).filter(
|
||||
(panel) => !FIXED_PANELS.includes(panel.url_path)
|
||||
);
|
||||
|
||||
allPanels.forEach((panel) => {
|
||||
const isDefaultPanel = panel.url_path === defaultPanel;
|
||||
|
||||
if (
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(!panel.title && panel.url_path !== defaultPanel) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path))
|
||||
!isDefaultPanel &&
|
||||
(!panel.title ||
|
||||
hiddenPanels.includes(panel.url_path) ||
|
||||
(panel.default_visible === false &&
|
||||
!panelsOrder.includes(panel.url_path)))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
(SHOW_AFTER_SPACER.includes(panel.url_path)
|
||||
(SHOW_AFTER_SPACER_PANELS.includes(panel.url_path)
|
||||
? afterSpacer
|
||||
: beforeSpacer
|
||||
).push(panel);
|
||||
@@ -251,10 +242,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
// Show the supervisor as being part of configuration
|
||||
const selectedPanel = this.route.path?.startsWith("/hassio/")
|
||||
? "config"
|
||||
: this.hass.panelUrl;
|
||||
const selectedPanel = this.hass.panelUrl;
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
@@ -397,9 +385,9 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
private _renderAllPanels(selectedPanel: string) {
|
||||
if (!this._panelOrder || !this._hiddenPanels) {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}
|
||||
><ha-spinner size="small"></ha-spinner
|
||||
></ha-fade-in>
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="small"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -413,7 +401,6 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
// prettier-ignore
|
||||
return html`
|
||||
<ha-md-list
|
||||
class="ha-scrollbar"
|
||||
@@ -422,61 +409,42 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
@scroll=${this._listboxScroll}
|
||||
@keydown=${this._listboxKeydown}
|
||||
>
|
||||
${this._renderPanels(beforeSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderPanels(beforeSpacer, selectedPanel)}
|
||||
${this._renderSpacer()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel, defaultPanel)}
|
||||
${this._renderExternalConfiguration()}
|
||||
${this._renderPanels(afterSpacer, selectedPanel)}
|
||||
${this.hass.user?.is_admin
|
||||
? this._renderConfiguration(selectedPanel)
|
||||
: this._renderExternalConfiguration()}
|
||||
</ha-md-list>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderPanels(
|
||||
panels: PanelInfo[],
|
||||
selectedPanel: string,
|
||||
defaultPanel: string
|
||||
) {
|
||||
private _renderPanels(panels: PanelInfo[], selectedPanel: string) {
|
||||
return panels.map((panel) =>
|
||||
this._renderPanel(
|
||||
panel.url_path,
|
||||
panel.url_path === defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title,
|
||||
panel.icon,
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
: undefined,
|
||||
selectedPanel
|
||||
)
|
||||
this._renderPanel(panel, panel.url_path === selectedPanel)
|
||||
);
|
||||
}
|
||||
|
||||
private _renderPanel(
|
||||
urlPath: string,
|
||||
title: string | null,
|
||||
icon: string | null | undefined,
|
||||
iconPath: string | null | undefined,
|
||||
selectedPanel: string
|
||||
) {
|
||||
return urlPath === "config"
|
||||
? this._renderConfiguration(title, selectedPanel)
|
||||
: html`
|
||||
<ha-md-list-item
|
||||
.href=${`/${urlPath}`}
|
||||
type="link"
|
||||
class=${classMap({
|
||||
selected: selectedPanel === urlPath,
|
||||
})}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
private _renderPanel(panel: PanelInfo, isSelected: boolean) {
|
||||
const title = getPanelTitle(this.hass, panel);
|
||||
const urlPath = panel.url_path;
|
||||
const icon = getPanelIcon(panel);
|
||||
const iconPath = getPanelIconPath(panel);
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
.href=${`/${urlPath}`}
|
||||
type="link"
|
||||
class=${classMap({ selected: isSelected })}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: html`<ha-icon slot="start" .icon=${icon}></ha-icon>`}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderDivider() {
|
||||
@@ -487,10 +455,15 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
return html`<div class="spacer" disabled></div>`;
|
||||
}
|
||||
|
||||
private _renderConfiguration(title: string | null, selectedPanel: string) {
|
||||
private _renderConfiguration(selectedPanel: string) {
|
||||
if (!this.hass.user?.is_admin) {
|
||||
return nothing;
|
||||
}
|
||||
const isSelected =
|
||||
selectedPanel === "config" || this.route.path?.startsWith("/hassio/");
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
class="configuration${selectedPanel === "config" ? " selected" : ""}"
|
||||
class="configuration ${classMap({ selected: isSelected })}"
|
||||
type="button"
|
||||
href="/config"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -504,15 +477,17 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
${this._updatesCount + this._issuesCount}
|
||||
</span>
|
||||
`
|
||||
: ""}
|
||||
<span class="item-text" slot="headline">${title}</span>
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("panel.config")}</span
|
||||
>
|
||||
${this.alwaysExpand && (this._updatesCount > 0 || this._issuesCount > 0)
|
||||
? html`
|
||||
<span class="badge" slot="end"
|
||||
>${this._updatesCount + this._issuesCount}</span
|
||||
>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
@@ -535,19 +510,20 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
? html`
|
||||
<span class="badge" slot="start"> ${notificationCount} </span>
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.localize("ui.notification_drawer.title")}</span
|
||||
>
|
||||
${this.alwaysExpand && notificationCount > 0
|
||||
? html`<span class="badge" slot="end">${notificationCount}</span>`
|
||||
: ""}
|
||||
: nothing}
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderUserItem(selectedPanel: string) {
|
||||
const isRTL = computeRTL(this.hass);
|
||||
const isSelected = selectedPanel === "profile";
|
||||
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@@ -555,7 +531,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
type="link"
|
||||
class=${classMap({
|
||||
user: true,
|
||||
selected: selectedPanel === "profile",
|
||||
selected: isSelected,
|
||||
rtl: isRTL,
|
||||
})}
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@@ -566,31 +542,30 @@ class HaSidebar extends SubscribeMixin(LitElement) {
|
||||
.user=${this.hass.user}
|
||||
.hass=${this.hass}
|
||||
></ha-user-badge>
|
||||
|
||||
<span class="item-text" slot="headline"
|
||||
>${this.hass.user ? this.hass.user.name : ""}</span
|
||||
>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.user ? this.hass.user.name : ""}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderExternalConfiguration() {
|
||||
return html`${!this.hass.user?.is_admin &&
|
||||
this.hass.auth.external?.config.hasSettingsScreen
|
||||
? html`
|
||||
<ha-md-list-item
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
type="button"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
`
|
||||
: ""}`;
|
||||
if (!this.hass.auth.external?.config.hasSettingsScreen) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-md-list-item
|
||||
@click=${this._handleExternalAppConfiguration}
|
||||
type="button"
|
||||
@mouseenter=${this._itemMouseEnter}
|
||||
@mouseleave=${this._itemMouseLeave}
|
||||
>
|
||||
<ha-svg-icon slot="start" .path=${mdiCellphoneCog}></ha-svg-icon>
|
||||
<span class="item-text" slot="headline">
|
||||
${this.hass.localize("ui.sidebar.external_app_configuration")}
|
||||
</span>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
}
|
||||
|
||||
private _handleExternalAppConfiguration(ev: Event) {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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) : "_";
|
||||
|
||||
@@ -7,8 +7,8 @@ export interface CoreFrontendUserData {
|
||||
}
|
||||
|
||||
export interface SidebarFrontendUserData {
|
||||
panelOrder: string[];
|
||||
hiddenPanels: string[];
|
||||
panelOrder?: string[];
|
||||
hiddenPanels?: string[];
|
||||
}
|
||||
|
||||
export interface CoreFrontendSystemData {
|
||||
|
||||
@@ -60,6 +60,7 @@ import type {
|
||||
|
||||
import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
|
||||
import { getTriggerDomain, getTriggerObjectId } from "./trigger";
|
||||
import { getConditionDomain, getConditionObjectId } from "./condition";
|
||||
|
||||
/** Icon to use when no icon specified for service. */
|
||||
export const DEFAULT_SERVICE_ICON = mdiRoomService;
|
||||
@@ -138,15 +139,25 @@ const resources: {
|
||||
all?: Promise<Record<string, TriggerIcons>>;
|
||||
domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
|
||||
};
|
||||
conditions: {
|
||||
all?: Promise<Record<string, ConditionIcons>>;
|
||||
domains: Record<string, ConditionIcons | Promise<ConditionIcons>>;
|
||||
};
|
||||
} = {
|
||||
entity: {},
|
||||
entity_component: {},
|
||||
services: { domains: {} },
|
||||
triggers: { domains: {} },
|
||||
conditions: { domains: {} },
|
||||
};
|
||||
|
||||
interface IconResources<
|
||||
T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
|
||||
T extends
|
||||
| ComponentIcons
|
||||
| PlatformIcons
|
||||
| ServiceIcons
|
||||
| TriggerIcons
|
||||
| ConditionIcons,
|
||||
> {
|
||||
resources: Record<string, T>;
|
||||
}
|
||||
@@ -195,17 +206,24 @@ type TriggerIcons = Record<
|
||||
{ trigger: string; sections?: Record<string, string> }
|
||||
>;
|
||||
|
||||
type ConditionIcons = Record<
|
||||
string,
|
||||
{ condition: string; sections?: Record<string, string> }
|
||||
>;
|
||||
|
||||
export type IconCategory =
|
||||
| "entity"
|
||||
| "entity_component"
|
||||
| "services"
|
||||
| "triggers";
|
||||
| "triggers"
|
||||
| "conditions";
|
||||
|
||||
interface CategoryType {
|
||||
entity: PlatformIcons;
|
||||
entity_component: ComponentIcons;
|
||||
services: ServiceIcons;
|
||||
triggers: TriggerIcons;
|
||||
conditions: ConditionIcons;
|
||||
}
|
||||
|
||||
export const getHassIcons = async <T extends IconCategory>(
|
||||
@@ -327,6 +345,13 @@ export const getTriggerIcons = async (
|
||||
): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> =>
|
||||
getCategoryIcons(hass, "triggers", domain, force);
|
||||
|
||||
export const getConditionIcons = async (
|
||||
hass: HomeAssistant,
|
||||
domain?: string,
|
||||
force = false
|
||||
): Promise<ConditionIcons | Record<string, ConditionIcons> | undefined> =>
|
||||
getCategoryIcons(hass, "conditions", domain, force);
|
||||
|
||||
// Cache for sorted range keys
|
||||
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
|
||||
|
||||
@@ -526,6 +551,25 @@ export const triggerIcon = async (
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const conditionIcon = async (
|
||||
hass: HomeAssistant,
|
||||
condition: string
|
||||
): Promise<string | undefined> => {
|
||||
let icon: string | undefined;
|
||||
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionIcons = await getConditionIcons(hass, domain);
|
||||
if (conditionIcons) {
|
||||
const conditionName = getConditionObjectId(condition);
|
||||
const condIcon = conditionIcons[conditionName] as ConditionIcons[string];
|
||||
icon = condIcon?.condition;
|
||||
}
|
||||
if (!icon) {
|
||||
icon = await domainIcon(hass, domain);
|
||||
}
|
||||
return icon;
|
||||
};
|
||||
|
||||
export const serviceIcon = async (
|
||||
hass: HomeAssistant,
|
||||
service: string
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
import {
|
||||
mdiAccount,
|
||||
mdiCalendar,
|
||||
mdiChartBox,
|
||||
mdiClipboardList,
|
||||
mdiFormatListBulletedType,
|
||||
mdiHammer,
|
||||
mdiLightningBolt,
|
||||
mdiPlayBoxMultiple,
|
||||
mdiTooltipAccount,
|
||||
mdiViewDashboard,
|
||||
} from "@mdi/js";
|
||||
import type { HomeAssistant, PanelInfo } from "../types";
|
||||
|
||||
/** Panel to show when no panel is picked. */
|
||||
@@ -60,7 +72,7 @@ export const getPanelTitleFromUrlPath = (
|
||||
return getPanelTitle(hass, panel);
|
||||
};
|
||||
|
||||
export const getPanelIcon = (panel: PanelInfo): string | null => {
|
||||
export const getPanelIcon = (panel: PanelInfo): string | undefined => {
|
||||
if (!panel.icon) {
|
||||
switch (panel.component_name) {
|
||||
case "profile":
|
||||
@@ -70,5 +82,24 @@ export const getPanelIcon = (panel: PanelInfo): string | null => {
|
||||
}
|
||||
}
|
||||
|
||||
return panel.icon;
|
||||
return panel.icon || undefined;
|
||||
};
|
||||
|
||||
export const PANEL_ICON_PATHS = {
|
||||
calendar: mdiCalendar,
|
||||
"developer-tools": mdiHammer,
|
||||
energy: mdiLightningBolt,
|
||||
history: mdiChartBox,
|
||||
logbook: mdiFormatListBulletedType,
|
||||
lovelace: mdiViewDashboard,
|
||||
profile: mdiAccount,
|
||||
map: mdiTooltipAccount,
|
||||
"media-browser": mdiPlayBoxMultiple,
|
||||
todo: mdiClipboardList,
|
||||
};
|
||||
|
||||
export const getPanelIconPath = (panel: PanelInfo): string | undefined =>
|
||||
PANEL_ICON_PATHS[panel.url_path];
|
||||
|
||||
export const FIXED_PANELS = ["profile", "config"];
|
||||
export const SHOW_AFTER_SPACER_PANELS = ["developer-tools"];
|
||||
|
||||
@@ -75,7 +75,8 @@ export type TranslationCategory =
|
||||
| "preview_features"
|
||||
| "selector"
|
||||
| "services"
|
||||
| "triggers";
|
||||
| "triggers"
|
||||
| "conditions";
|
||||
|
||||
export const subscribeTranslationPreferences = (
|
||||
hass: HomeAssistant,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import "@material/mwc-linear-progress/mwc-linear-progress";
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import { mdiClose, mdiDotsVertical, mdiRestart } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -9,18 +9,30 @@ import "../../components/ha-dialog-header";
|
||||
import "../../components/ha-fade-in";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-items-display-editor";
|
||||
import type { DisplayValue } from "../../components/ha-items-display-editor";
|
||||
import type {
|
||||
DisplayItem,
|
||||
DisplayValue,
|
||||
} from "../../components/ha-items-display-editor";
|
||||
import "../../components/ha-md-button-menu";
|
||||
import "../../components/ha-md-dialog";
|
||||
import type { HaMdDialog } from "../../components/ha-md-dialog";
|
||||
import { computePanels, PANEL_ICONS } from "../../components/ha-sidebar";
|
||||
import "../../components/ha-md-menu-item";
|
||||
import { computePanels } from "../../components/ha-sidebar";
|
||||
import "../../components/ha-spinner";
|
||||
import "../../components/ha-svg-icon";
|
||||
import {
|
||||
fetchFrontendUserData,
|
||||
saveFrontendUserData,
|
||||
} from "../../data/frontend";
|
||||
import {
|
||||
getDefaultPanelUrlPath,
|
||||
getPanelIcon,
|
||||
getPanelIconPath,
|
||||
getPanelTitle,
|
||||
SHOW_AFTER_SPACER_PANELS,
|
||||
} from "../../data/panel";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showConfirmationDialog } from "../generic/show-dialog-box";
|
||||
import { getDefaultPanelUrlPath } from "../../data/panel";
|
||||
|
||||
@customElement("dialog-edit-sidebar")
|
||||
class DialogEditSidebar extends LitElement {
|
||||
@@ -105,48 +117,53 @@ class DialogEditSidebar extends LitElement {
|
||||
this.hass.locale
|
||||
);
|
||||
|
||||
// Add default hidden panels that are missing in hidden
|
||||
const orderSet = new Set(this._order);
|
||||
const hiddenSet = new Set(this._hidden);
|
||||
|
||||
for (const panel of panels) {
|
||||
if (
|
||||
panel.default_visible === false &&
|
||||
!this._order.includes(panel.url_path) &&
|
||||
!this._hidden.includes(panel.url_path)
|
||||
!orderSet.has(panel.url_path) &&
|
||||
!hiddenSet.has(panel.url_path)
|
||||
) {
|
||||
this._hidden.push(panel.url_path);
|
||||
hiddenSet.add(panel.url_path);
|
||||
}
|
||||
}
|
||||
|
||||
if (hiddenSet.has(defaultPanel)) {
|
||||
hiddenSet.delete(defaultPanel);
|
||||
}
|
||||
|
||||
const hiddenPanels = Array.from(hiddenSet);
|
||||
|
||||
const items = [
|
||||
...beforeSpacer,
|
||||
...panels.filter((panel) => this._hidden!.includes(panel.url_path)),
|
||||
...afterSpacer.filter((panel) => panel.url_path !== "config"),
|
||||
].map((panel) => ({
|
||||
...panels.filter((panel) => hiddenPanels.includes(panel.url_path)),
|
||||
...afterSpacer,
|
||||
].map<DisplayItem>((panel) => ({
|
||||
value: panel.url_path,
|
||||
label:
|
||||
panel.url_path === defaultPanel
|
||||
? panel.title || this.hass.localize("panel.states")
|
||||
: this.hass.localize(`panel.${panel.title}`) || panel.title || "?",
|
||||
icon: panel.icon || undefined,
|
||||
iconPath:
|
||||
panel.url_path === defaultPanel && !panel.icon
|
||||
? PANEL_ICONS.lovelace
|
||||
: panel.url_path in PANEL_ICONS
|
||||
? PANEL_ICONS[panel.url_path]
|
||||
: undefined,
|
||||
disableSorting: panel.url_path === "developer-tools",
|
||||
(getPanelTitle(this.hass, panel) || panel.url_path) +
|
||||
`${defaultPanel === panel.url_path ? " (default)" : ""}`,
|
||||
icon: getPanelIcon(panel),
|
||||
iconPath: getPanelIconPath(panel),
|
||||
disableSorting: SHOW_AFTER_SPACER_PANELS.includes(panel.url_path),
|
||||
disableHiding: panel.url_path === defaultPanel,
|
||||
}));
|
||||
|
||||
return html`<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.value=${{
|
||||
order: this._order,
|
||||
hidden: this._hidden,
|
||||
}}
|
||||
.items=${items}
|
||||
@value-changed=${this._changed}
|
||||
dont-sort-visible
|
||||
>
|
||||
</ha-items-display-editor>`;
|
||||
return html`
|
||||
<ha-items-display-editor
|
||||
.hass=${this.hass}
|
||||
.value=${{
|
||||
order: this._order,
|
||||
hidden: hiddenPanels,
|
||||
}}
|
||||
.items=${items}
|
||||
@value-changed=${this._changed}
|
||||
dont-sort-visible
|
||||
>
|
||||
</ha-items-display-editor>
|
||||
`;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -171,6 +188,22 @@ class DialogEditSidebar extends LitElement {
|
||||
>${this.hass.localize("ui.sidebar.edit_subtitle")}</span
|
||||
>`
|
||||
: nothing}
|
||||
<ha-md-button-menu
|
||||
slot="actionItems"
|
||||
positioning="popover"
|
||||
anchor-corner="end-end"
|
||||
menu-corner="start-end"
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
<ha-md-menu-item .clickAction=${this._resetToDefaults}>
|
||||
<ha-svg-icon slot="start" .path=${mdiRestart}></ha-svg-icon>
|
||||
${this.hass.localize("ui.sidebar.reset_to_defaults")}
|
||||
</ha-md-menu-item>
|
||||
</ha-md-button-menu>
|
||||
</ha-dialog-header>
|
||||
<div slot="content" class="content">${this._renderContent()}</div>
|
||||
<div slot="actions">
|
||||
@@ -194,6 +227,26 @@ class DialogEditSidebar extends LitElement {
|
||||
this._hidden = [...hidden];
|
||||
}
|
||||
|
||||
private _resetToDefaults = async () => {
|
||||
const confirmation = await showConfirmationDialog(this, {
|
||||
text: this.hass.localize("ui.sidebar.reset_confirmation"),
|
||||
confirmText: this.hass.localize("ui.common.reset"),
|
||||
});
|
||||
|
||||
if (!confirmation) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._order = [];
|
||||
this._hidden = [];
|
||||
try {
|
||||
await saveFrontendUserData(this.hass.connection, "sidebar", {});
|
||||
} catch (err: any) {
|
||||
this._error = err.message || err;
|
||||
}
|
||||
this.closeDialog();
|
||||
};
|
||||
|
||||
private async _save() {
|
||||
if (this._migrateToUserData) {
|
||||
const confirmation = await showConfirmationDialog(this, {
|
||||
|
||||
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,19 +1,29 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { stringCompare } from "../../../../../common/string/compare";
|
||||
import { stopPropagation } from "../../../../../common/dom/stop_propagation";
|
||||
import { stringCompare } from "../../../../../common/string/compare";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import { CONDITION_ICONS } from "../../../../../components/ha-condition-icon";
|
||||
import "../../../../../components/ha-list-item";
|
||||
import "../../../../../components/ha-select";
|
||||
import type { HaSelect } from "../../../../../components/ha-select";
|
||||
import type { Condition } from "../../../../../data/automation";
|
||||
import {
|
||||
DYNAMIC_PREFIX,
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type Condition,
|
||||
} from "../../../../../data/automation";
|
||||
import type { ConditionDescriptions } from "../../../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
CONDITION_ICONS,
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
subscribeConditions,
|
||||
} from "../../../../../data/condition";
|
||||
import type { Entries, HomeAssistant } from "../../../../../types";
|
||||
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../condition/ha-automation-condition-editor";
|
||||
import type HaAutomationConditionEditor from "../../condition/ha-automation-condition-editor";
|
||||
import "../../condition/types/ha-automation-condition-and";
|
||||
@@ -30,7 +40,10 @@ import "../../condition/types/ha-automation-condition-zone";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-condition")
|
||||
export class HaConditionAction extends LitElement implements ActionElement {
|
||||
export class HaConditionAction
|
||||
extends SubscribeMixin(LitElement)
|
||||
implements ActionElement
|
||||
{
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -43,6 +56,8 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "indent" }) public indent = false;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@query("ha-automation-condition-editor")
|
||||
private _conditionEditor?: HaAutomationConditionEditor;
|
||||
|
||||
@@ -50,6 +65,21 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
return { condition: "state" };
|
||||
}
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeConditions(this.hass, (conditions) =>
|
||||
this._addConditions(conditions)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _addConditions(conditions: ConditionDescriptions) {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const buildingBlock = CONDITION_BUILDING_BLOCKS.includes(
|
||||
this.action.condition
|
||||
@@ -64,19 +94,25 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
"ui.panel.config.automation.editor.conditions.type_select"
|
||||
)}
|
||||
.disabled=${this.disabled}
|
||||
.value=${this.action.condition}
|
||||
.value=${this.action.condition in this._conditionDescriptions
|
||||
? `${DYNAMIC_PREFIX}${this.action.condition}`
|
||||
: this.action.condition}
|
||||
naturalMenuWidth
|
||||
@selected=${this._typeChanged}
|
||||
@closed=${stopPropagation}
|
||||
>
|
||||
${this._processedTypes(this.hass.localize).map(
|
||||
([opt, label, icon]) => html`
|
||||
${this._processedTypes(
|
||||
this._conditionDescriptions,
|
||||
this.hass.localize
|
||||
).map(
|
||||
([opt, label, condition]) => html`
|
||||
<ha-list-item .value=${opt} graphic="icon">
|
||||
${label}<ha-svg-icon
|
||||
${label}
|
||||
<ha-condition-icon
|
||||
slot="graphic"
|
||||
.path=${icon}
|
||||
></ha-svg-icon
|
||||
></ha-list-item>
|
||||
.condition=${condition}
|
||||
></ha-condition-icon>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}
|
||||
</ha-select>
|
||||
@@ -88,11 +124,14 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
? html`
|
||||
<ha-automation-condition-editor
|
||||
.condition=${this.action}
|
||||
.description=${this._conditionDescriptions[this.action.condition]}
|
||||
.disabled=${this.disabled}
|
||||
.hass=${this.hass}
|
||||
@value-changed=${this._conditionChanged}
|
||||
.narrow=${this.narrow}
|
||||
.uiSupported=${this._uiSupported(this.action.condition)}
|
||||
.uiSupported=${this._uiSupported(
|
||||
this._getType(this.action, this._conditionDescriptions)
|
||||
)}
|
||||
.indent=${this.indent}
|
||||
action
|
||||
></ha-automation-condition-editor>
|
||||
@@ -102,19 +141,46 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
private _processedTypes = memoizeOne(
|
||||
(localize: LocalizeFunc): [string, string, string][] =>
|
||||
(Object.entries(CONDITION_ICONS) as Entries<typeof CONDITION_ICONS>)
|
||||
.map(
|
||||
([condition, icon]) =>
|
||||
[
|
||||
condition,
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
|
||||
),
|
||||
icon,
|
||||
] as [string, string, string]
|
||||
)
|
||||
.sort((a, b) => stringCompare(a[1], b[1], this.hass.locale.language))
|
||||
(
|
||||
conditionDescriptions: ConditionDescriptions,
|
||||
localize: LocalizeFunc
|
||||
): [string, string, string][] => {
|
||||
const legacy = (
|
||||
Object.keys(CONDITION_ICONS) as (keyof typeof CONDITION_ICONS)[]
|
||||
).map(
|
||||
(condition) =>
|
||||
[
|
||||
condition,
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.conditions.type.${condition}.label`
|
||||
),
|
||||
condition,
|
||||
] as [string, string, string]
|
||||
);
|
||||
const platform = Object.keys(conditionDescriptions).map((condition) => {
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionObjId = getConditionObjectId(condition);
|
||||
return [
|
||||
`${DYNAMIC_PREFIX}${condition}`,
|
||||
localize(`component.${domain}.conditions.${conditionObjId}.name`) ||
|
||||
condition,
|
||||
condition,
|
||||
] as [string, string, string];
|
||||
});
|
||||
return [...legacy, ...platform].sort((a, b) =>
|
||||
stringCompare(a[1], b[1], this.hass.locale.language)
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
private _getType = memoizeOne(
|
||||
(condition: Condition, conditionDescriptions: ConditionDescriptions) => {
|
||||
if (condition.condition in conditionDescriptions) {
|
||||
return "platform";
|
||||
}
|
||||
|
||||
return condition.condition;
|
||||
}
|
||||
);
|
||||
|
||||
private _conditionChanged(ev: CustomEvent) {
|
||||
@@ -132,6 +198,18 @@ export class HaConditionAction extends LitElement implements ActionElement {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDynamic(type)) {
|
||||
const value = getValueFromDynamic(type);
|
||||
if (value !== this.action.condition) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
condition: value,
|
||||
},
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const elClass = customElements.get(
|
||||
`ha-automation-condition-${type}`
|
||||
) as CustomElementConstructor & {
|
||||
|
||||
@@ -56,12 +56,19 @@ import {
|
||||
type AutomationElementGroup,
|
||||
type AutomationElementGroupCollection,
|
||||
} from "../../../data/automation";
|
||||
import type { ConditionDescriptions } from "../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS_GROUP,
|
||||
CONDITION_COLLECTIONS,
|
||||
CONDITION_ICONS,
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
subscribeConditions,
|
||||
} from "../../../data/condition";
|
||||
import { getServiceIcons, getTriggerIcons } from "../../../data/icons";
|
||||
import {
|
||||
getConditionIcons,
|
||||
getServiceIcons,
|
||||
getTriggerIcons,
|
||||
} from "../../../data/icons";
|
||||
import type { IntegrationManifest } from "../../../data/integration";
|
||||
import {
|
||||
domainToName,
|
||||
@@ -82,6 +89,7 @@ import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
|
||||
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
|
||||
import { CONDITION_ICONS } from "../../../components/ha-condition-icon";
|
||||
|
||||
const TYPES = {
|
||||
trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS },
|
||||
@@ -119,7 +127,7 @@ const ENTITY_DOMAINS_OTHER = new Set([
|
||||
|
||||
const ENTITY_DOMAINS_MAIN = new Set(["notify"]);
|
||||
|
||||
const ACTION_SERVICE_KEYWORDS = ["dynamicGroups", "helpers", "other"];
|
||||
const DYNAMIC_KEYWORDS = ["dynamicGroups", "helpers", "other"];
|
||||
|
||||
@customElement("add-automation-element-dialog")
|
||||
class DialogAddAutomationElement
|
||||
@@ -152,6 +160,8 @@ class DialogAddAutomationElement
|
||||
|
||||
@state() private _triggerDescriptions: TriggerDescriptions = {};
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@query(".items ha-md-list ha-md-list-item")
|
||||
private _itemsListFirstElement?: HaMdList;
|
||||
|
||||
@@ -169,15 +179,15 @@ class DialogAddAutomationElement
|
||||
|
||||
this.addKeyboardShortcuts();
|
||||
|
||||
this._unsubscribe();
|
||||
this._fetchManifests();
|
||||
|
||||
if (this._params?.type === "action") {
|
||||
this.hass.loadBackendTranslation("services");
|
||||
this._fetchManifests();
|
||||
this._calculateUsedDomains();
|
||||
getServiceIcons(this.hass);
|
||||
}
|
||||
if (this._params?.type === "trigger") {
|
||||
} else if (this._params?.type === "trigger") {
|
||||
this.hass.loadBackendTranslation("triggers");
|
||||
this._fetchManifests();
|
||||
getTriggerIcons(this.hass);
|
||||
this._unsub = subscribeTriggers(this.hass, (triggers) => {
|
||||
this._triggerDescriptions = {
|
||||
@@ -185,7 +195,17 @@ class DialogAddAutomationElement
|
||||
...triggers,
|
||||
};
|
||||
});
|
||||
} else if (this._params?.type === "condition") {
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
getConditionIcons(this.hass);
|
||||
this._unsub = subscribeConditions(this.hass, (conditions) => {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
this._fullScreen = matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
@@ -199,10 +219,7 @@ class DialogAddAutomationElement
|
||||
|
||||
public closeDialog() {
|
||||
this.removeKeyboardShortcuts();
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
this._unsubscribe();
|
||||
if (this._params) {
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
@@ -219,6 +236,13 @@ class DialogAddAutomationElement
|
||||
return true;
|
||||
}
|
||||
|
||||
private _unsubscribe() {
|
||||
if (this._unsub) {
|
||||
this._unsub.then((unsub) => unsub());
|
||||
this._unsub = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private _getGroups = (
|
||||
type: AddAutomationElementDialogParams["type"],
|
||||
group?: string,
|
||||
@@ -348,8 +372,11 @@ class DialogAddAutomationElement
|
||||
items.push(
|
||||
...this._triggers(localize, this._triggerDescriptions, manifests)
|
||||
);
|
||||
}
|
||||
if (type === "action") {
|
||||
} else if (type === "condition") {
|
||||
items.push(
|
||||
...this._conditions(localize, this._conditionDescriptions, manifests)
|
||||
);
|
||||
} else if (type === "action") {
|
||||
items.push(...this._services(localize, services, manifests));
|
||||
}
|
||||
return items;
|
||||
@@ -372,6 +399,7 @@ class DialogAddAutomationElement
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"],
|
||||
triggerDescriptions: TriggerDescriptions,
|
||||
conditionDescriptions: ConditionDescriptions,
|
||||
manifests?: DomainManifestLookup
|
||||
): {
|
||||
titleKey?: LocalizeKeys;
|
||||
@@ -383,35 +411,10 @@ class DialogAddAutomationElement
|
||||
let collectionGroups = Object.entries(collection.groups);
|
||||
const groups: ListItem[] = [];
|
||||
|
||||
if (
|
||||
type === "action" &&
|
||||
Object.keys(collection.groups).some((item) =>
|
||||
ACTION_SERVICE_KEYWORDS.includes(item)
|
||||
)
|
||||
) {
|
||||
groups.push(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
collection.groups.dynamicGroups
|
||||
? undefined
|
||||
: collection.groups.helpers
|
||||
? "helper"
|
||||
: "other"
|
||||
)
|
||||
);
|
||||
|
||||
collectionGroups = collectionGroups.filter(
|
||||
([key]) => !ACTION_SERVICE_KEYWORDS.includes(key)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
type === "trigger" &&
|
||||
Object.keys(collection.groups).some((item) =>
|
||||
ACTION_SERVICE_KEYWORDS.includes(item)
|
||||
DYNAMIC_KEYWORDS.includes(item)
|
||||
)
|
||||
) {
|
||||
groups.push(
|
||||
@@ -429,7 +432,53 @@ class DialogAddAutomationElement
|
||||
);
|
||||
|
||||
collectionGroups = collectionGroups.filter(
|
||||
([key]) => !ACTION_SERVICE_KEYWORDS.includes(key)
|
||||
([key]) => !DYNAMIC_KEYWORDS.includes(key)
|
||||
);
|
||||
} else if (
|
||||
type === "condition" &&
|
||||
Object.keys(collection.groups).some((item) =>
|
||||
DYNAMIC_KEYWORDS.includes(item)
|
||||
)
|
||||
) {
|
||||
groups.push(
|
||||
...this._conditionGroups(
|
||||
localize,
|
||||
conditionDescriptions,
|
||||
manifests,
|
||||
domains,
|
||||
collection.groups.dynamicGroups
|
||||
? undefined
|
||||
: collection.groups.helpers
|
||||
? "helper"
|
||||
: "other"
|
||||
)
|
||||
);
|
||||
|
||||
collectionGroups = collectionGroups.filter(
|
||||
([key]) => !DYNAMIC_KEYWORDS.includes(key)
|
||||
);
|
||||
} else if (
|
||||
type === "action" &&
|
||||
Object.keys(collection.groups).some((item) =>
|
||||
DYNAMIC_KEYWORDS.includes(item)
|
||||
)
|
||||
) {
|
||||
groups.push(
|
||||
...this._serviceGroups(
|
||||
localize,
|
||||
services,
|
||||
manifests,
|
||||
domains,
|
||||
collection.groups.dynamicGroups
|
||||
? undefined
|
||||
: collection.groups.helpers
|
||||
? "helper"
|
||||
: "other"
|
||||
)
|
||||
);
|
||||
|
||||
collectionGroups = collectionGroups.filter(
|
||||
([key]) => !DYNAMIC_KEYWORDS.includes(key)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -487,10 +536,6 @@ class DialogAddAutomationElement
|
||||
services: HomeAssistant["services"],
|
||||
manifests?: DomainManifestLookup
|
||||
): ListItem[] => {
|
||||
if (type === "action" && isDynamic(group)) {
|
||||
return this._services(localize, services, manifests, group);
|
||||
}
|
||||
|
||||
if (type === "trigger" && isDynamic(group)) {
|
||||
return this._triggers(
|
||||
localize,
|
||||
@@ -499,6 +544,17 @@ class DialogAddAutomationElement
|
||||
group
|
||||
);
|
||||
}
|
||||
if (type === "condition" && isDynamic(group)) {
|
||||
return this._conditions(
|
||||
localize,
|
||||
this._conditionDescriptions,
|
||||
manifests,
|
||||
group
|
||||
);
|
||||
}
|
||||
if (type === "action" && isDynamic(group)) {
|
||||
return this._services(localize, services, manifests, group);
|
||||
}
|
||||
|
||||
const groups = this._getGroups(type, group, collectionIndex);
|
||||
|
||||
@@ -688,6 +744,102 @@ class DialogAddAutomationElement
|
||||
}
|
||||
);
|
||||
|
||||
private _conditionGroups = (
|
||||
localize: LocalizeFunc,
|
||||
conditions: ConditionDescriptions,
|
||||
manifests: DomainManifestLookup | undefined,
|
||||
domains: Set<string> | undefined,
|
||||
type: "helper" | "other" | undefined
|
||||
): ListItem[] => {
|
||||
if (!conditions || !manifests) {
|
||||
return [];
|
||||
}
|
||||
const result: ListItem[] = [];
|
||||
const addedDomains = new Set<string>();
|
||||
Object.keys(conditions).forEach((condition) => {
|
||||
const domain = getConditionDomain(condition);
|
||||
|
||||
if (addedDomains.has(domain)) {
|
||||
return;
|
||||
}
|
||||
addedDomains.add(domain);
|
||||
|
||||
const manifest = manifests[domain];
|
||||
const domainUsed = !domains ? true : domains.has(domain);
|
||||
|
||||
if (
|
||||
(type === undefined &&
|
||||
(ENTITY_DOMAINS_MAIN.has(domain) ||
|
||||
(manifest?.integration_type === "entity" &&
|
||||
domainUsed &&
|
||||
!ENTITY_DOMAINS_OTHER.has(domain)))) ||
|
||||
(type === "helper" && manifest?.integration_type === "helper") ||
|
||||
(type === "other" &&
|
||||
!ENTITY_DOMAINS_MAIN.has(domain) &&
|
||||
(ENTITY_DOMAINS_OTHER.has(domain) ||
|
||||
(!domainUsed && manifest?.integration_type === "entity") ||
|
||||
!["helper", "entity"].includes(manifest?.integration_type || "")))
|
||||
) {
|
||||
result.push({
|
||||
icon: html`
|
||||
<ha-domain-icon
|
||||
.hass=${this.hass}
|
||||
.domain=${domain}
|
||||
brand-fallback
|
||||
></ha-domain-icon>
|
||||
`,
|
||||
key: `${DYNAMIC_PREFIX}${domain}`,
|
||||
name: domainToName(localize, domain, manifest),
|
||||
description: "",
|
||||
});
|
||||
}
|
||||
});
|
||||
return result.sort((a, b) =>
|
||||
stringCompare(a.name, b.name, this.hass.locale.language)
|
||||
);
|
||||
};
|
||||
|
||||
private _conditions = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
conditions: ConditionDescriptions,
|
||||
_manifests: DomainManifestLookup | undefined,
|
||||
group?: string
|
||||
): ListItem[] => {
|
||||
if (!conditions) {
|
||||
return [];
|
||||
}
|
||||
const result: ListItem[] = [];
|
||||
|
||||
for (const condition of Object.keys(conditions)) {
|
||||
const domain = getConditionDomain(condition);
|
||||
const conditionName = getConditionObjectId(condition);
|
||||
|
||||
if (group && group !== `${DYNAMIC_PREFIX}${domain}`) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.push({
|
||||
icon: html`
|
||||
<ha-condition-icon
|
||||
.hass=${this.hass}
|
||||
.condition=${condition}
|
||||
></ha-condition-icon>
|
||||
`,
|
||||
key: `${DYNAMIC_PREFIX}${condition}`,
|
||||
name:
|
||||
localize(`component.${domain}.conditions.${conditionName}.name`) ||
|
||||
condition,
|
||||
description:
|
||||
localize(
|
||||
`component.${domain}.conditions.${conditionName}.description`
|
||||
) || condition,
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
);
|
||||
|
||||
private _services = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
@@ -832,6 +984,7 @@ class DialogAddAutomationElement
|
||||
this.hass.localize,
|
||||
this.hass.services,
|
||||
this._triggerDescriptions,
|
||||
this._conditionDescriptions,
|
||||
this._manifests
|
||||
);
|
||||
|
||||
@@ -1136,6 +1289,7 @@ class DialogAddAutomationElement
|
||||
super.disconnectedCallback();
|
||||
window.removeEventListener("resize", this._updateNarrow);
|
||||
this._removeSearchKeybindings();
|
||||
this._unsubscribe();
|
||||
}
|
||||
|
||||
private _updateNarrow = () => {
|
||||
|
||||
@@ -8,11 +8,13 @@ import "../../../../components/ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||
import type { Condition } from "../../../../data/automation";
|
||||
import { expandConditionWithShorthand } from "../../../../data/automation";
|
||||
import type { ConditionDescription } from "../../../../data/condition";
|
||||
import { COLLAPSIBLE_CONDITION_ELEMENTS } from "../../../../data/condition";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { editorStyles, indentStyle } from "../styles";
|
||||
import type { ConditionElement } from "./ha-automation-condition-row";
|
||||
import "./types/ha-automation-condition-platform";
|
||||
|
||||
@customElement("ha-automation-condition-editor")
|
||||
export default class HaAutomationConditionEditor extends LitElement {
|
||||
@@ -35,6 +37,8 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
@property({ type: Boolean, attribute: "supported" }) public uiSupported =
|
||||
false;
|
||||
|
||||
@property({ attribute: false }) public description?: ConditionDescription;
|
||||
|
||||
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
|
||||
|
||||
@query(COLLAPSIBLE_CONDITION_ELEMENTS.join(", "))
|
||||
@@ -83,16 +87,23 @@ export default class HaAutomationConditionEditor extends LitElement {
|
||||
`
|
||||
: html`
|
||||
<div @value-changed=${this._onUiChanged}>
|
||||
${dynamicElement(
|
||||
`ha-automation-condition-${condition.condition}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
condition: condition,
|
||||
disabled: this.disabled,
|
||||
optionsInSidebar: this.indent,
|
||||
narrow: this.narrow,
|
||||
}
|
||||
)}
|
||||
${this.description
|
||||
? html`<ha-automation-condition-platform
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.description=${this.description}
|
||||
.disabled=${this.disabled}
|
||||
></ha-automation-condition-platform>`
|
||||
: dynamicElement(
|
||||
`ha-automation-condition-${condition.condition}`,
|
||||
{
|
||||
hass: this.hass,
|
||||
condition: condition,
|
||||
disabled: this.disabled,
|
||||
optionsInSidebar: this.indent,
|
||||
narrow: this.narrow,
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
|
||||
@@ -32,6 +32,7 @@ import { copyToClipboard } from "../../../../common/util/copy-clipboard";
|
||||
import "../../../../components/ha-automation-row";
|
||||
import type { HaAutomationRow } from "../../../../components/ha-automation-row";
|
||||
import "../../../../components/ha-card";
|
||||
import "../../../../components/ha-condition-icon";
|
||||
import "../../../../components/ha-expansion-panel";
|
||||
import "../../../../components/ha-icon-button";
|
||||
import "../../../../components/ha-md-button-menu";
|
||||
@@ -44,10 +45,8 @@ import type {
|
||||
} from "../../../../data/automation";
|
||||
import { isCondition, testCondition } from "../../../../data/automation";
|
||||
import { describeCondition } from "../../../../data/automation_i18n";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
CONDITION_ICONS,
|
||||
} from "../../../../data/condition";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import { validateConfig } from "../../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
|
||||
@@ -130,6 +129,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@state() private _warnings?: string[];
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@property({ type: Boolean, attribute: "sidebar" })
|
||||
public optionsInSidebar = false;
|
||||
|
||||
@@ -179,11 +181,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
private _renderRow() {
|
||||
return html`
|
||||
<ha-svg-icon
|
||||
<ha-condition-icon
|
||||
slot="leading-icon"
|
||||
class="condition-icon"
|
||||
.path=${CONDITION_ICONS[this.condition.condition]}
|
||||
></ha-svg-icon>
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition.condition}
|
||||
></ha-condition-icon>
|
||||
<h3 slot="header">
|
||||
${capitalizeFirstLetter(
|
||||
describeCondition(this.condition, this.hass, this._entityReg)
|
||||
@@ -395,9 +397,14 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
<ha-automation-condition-editor
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.description=${this.conditionDescriptions[
|
||||
this.condition.condition
|
||||
]}
|
||||
.disabled=${this.disabled}
|
||||
.yamlMode=${this._yamlMode}
|
||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
||||
.uiSupported=${this._uiSupported(
|
||||
this._getType(this.condition, this.conditionDescriptions)
|
||||
)}
|
||||
.narrow=${this.narrow}
|
||||
@ui-mode-not-available=${this._handleUiModeNotAvailable}
|
||||
></ha-automation-condition-editor>`
|
||||
@@ -476,7 +483,9 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.condition=${this.condition}
|
||||
.disabled=${this.disabled}
|
||||
.uiSupported=${this._uiSupported(this.condition.condition)}
|
||||
.uiSupported=${this._uiSupported(
|
||||
this._getType(this.condition, this.conditionDescriptions)
|
||||
)}
|
||||
indent
|
||||
.selected=${this._selected}
|
||||
.narrow=${this.narrow}
|
||||
@@ -786,7 +795,10 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
cut: this._cutCondition,
|
||||
test: this._testCondition,
|
||||
config: sidebarCondition,
|
||||
uiSupported: this._uiSupported(sidebarCondition.condition),
|
||||
uiSupported: this._uiSupported(
|
||||
this._getType(sidebarCondition, this.conditionDescriptions)
|
||||
),
|
||||
description: this.conditionDescriptions[sidebarCondition.condition],
|
||||
yamlMode: this._yamlMode,
|
||||
} satisfies ConditionSidebarConfig);
|
||||
this._selected = true;
|
||||
@@ -802,6 +814,16 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private _getType = memoizeOne(
|
||||
(condition: Condition, conditionDescriptions: ConditionDescriptions) => {
|
||||
if (condition.condition in conditionDescriptions) {
|
||||
return "platform";
|
||||
}
|
||||
|
||||
return condition.condition;
|
||||
}
|
||||
);
|
||||
|
||||
private _uiSupported = memoizeOne(
|
||||
(type: string) =>
|
||||
customElements.get(`ha-automation-condition-${type}`) !== undefined
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, queryAll, state } from "lit/decorators";
|
||||
import { repeat } from "lit/directives/repeat";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stopPropagation } from "../../../../common/dom/stop_propagation";
|
||||
@@ -12,11 +13,18 @@ import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-button-menu";
|
||||
import "../../../../components/ha-sortable";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import type {
|
||||
AutomationClipboard,
|
||||
Condition,
|
||||
import {
|
||||
getValueFromDynamic,
|
||||
isDynamic,
|
||||
type AutomationClipboard,
|
||||
type Condition,
|
||||
} from "../../../../data/automation";
|
||||
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
|
||||
import type { ConditionDescriptions } from "../../../../data/condition";
|
||||
import {
|
||||
CONDITION_BUILDING_BLOCKS,
|
||||
subscribeConditions,
|
||||
} from "../../../../data/condition";
|
||||
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
PASTE_VALUE,
|
||||
@@ -25,10 +33,9 @@ import {
|
||||
import { automationRowsStyles } from "../styles";
|
||||
import "./ha-automation-condition-row";
|
||||
import type HaAutomationConditionRow from "./ha-automation-condition-row";
|
||||
import { ensureArray } from "../../../../common/array/ensure-array";
|
||||
|
||||
@customElement("ha-automation-condition")
|
||||
export default class HaAutomationCondition extends LitElement {
|
||||
export default class HaAutomationCondition extends SubscribeMixin(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public conditions!: Condition[];
|
||||
@@ -46,6 +53,8 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
@state() private _rowSortSelected?: number;
|
||||
|
||||
@state() private _conditionDescriptions: ConditionDescriptions = {};
|
||||
|
||||
@state()
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
@@ -64,6 +73,26 @@ export default class HaAutomationCondition extends LitElement {
|
||||
|
||||
private _conditionKeys = new WeakMap<Condition, string>();
|
||||
|
||||
protected hassSubscribe() {
|
||||
return [
|
||||
subscribeConditions(this.hass, (conditions) =>
|
||||
this._addConditions(conditions)
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
private _addConditions(conditions: ConditionDescriptions) {
|
||||
this._conditionDescriptions = {
|
||||
...this._conditionDescriptions,
|
||||
...conditions,
|
||||
};
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues) {
|
||||
if (!changedProperties.has("conditions")) {
|
||||
return;
|
||||
@@ -168,6 +197,7 @@ export default class HaAutomationCondition extends LitElement {
|
||||
.last=${idx === this.conditions.length - 1}
|
||||
.totalConditions=${this.conditions.length}
|
||||
.condition=${cond}
|
||||
.conditionDescriptions=${this._conditionDescriptions}
|
||||
.disabled=${this.disabled}
|
||||
.narrow=${this.narrow}
|
||||
@duplicate=${this._duplicateCondition}
|
||||
@@ -237,6 +267,10 @@ export default class HaAutomationCondition extends LitElement {
|
||||
conditions = this.conditions.concat(
|
||||
deepClone(this._clipboard!.condition)
|
||||
);
|
||||
} else if (isDynamic(value)) {
|
||||
conditions = this.conditions.concat({
|
||||
condition: getValueFromDynamic(value),
|
||||
});
|
||||
} else {
|
||||
const condition = value as Condition["condition"];
|
||||
const elClass = customElements.get(
|
||||
|
||||
@@ -0,0 +1,416 @@
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import { computeDomain } from "../../../../../common/entity/compute_domain";
|
||||
import "../../../../../components/ha-checkbox";
|
||||
import "../../../../../components/ha-selector/ha-selector";
|
||||
import "../../../../../components/ha-settings-row";
|
||||
import type { PlatformCondition } from "../../../../../data/automation";
|
||||
import {
|
||||
getConditionDomain,
|
||||
getConditionObjectId,
|
||||
type ConditionDescription,
|
||||
} from "../../../../../data/condition";
|
||||
import type { IntegrationManifest } from "../../../../../data/integration";
|
||||
import { fetchIntegrationManifest } from "../../../../../data/integration";
|
||||
import type { TargetSelector } from "../../../../../data/selector";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||
|
||||
const showOptionalToggle = (field: ConditionDescription["fields"][string]) =>
|
||||
field.selector &&
|
||||
!field.required &&
|
||||
!("boolean" in field.selector && field.default);
|
||||
|
||||
@customElement("ha-automation-condition-platform")
|
||||
export class HaPlatformCondition extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public condition!: PlatformCondition;
|
||||
|
||||
@property({ attribute: false }) public description?: ConditionDescription;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@state() private _checkedKeys = new Set();
|
||||
|
||||
@state() private _manifest?: IntegrationManifest;
|
||||
|
||||
public static get defaultConfig(): PlatformCondition {
|
||||
return { condition: "" };
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (!this.hasUpdated) {
|
||||
this.hass.loadBackendTranslation("conditions");
|
||||
this.hass.loadBackendTranslation("selector");
|
||||
}
|
||||
if (!changedProperties.has("condition")) {
|
||||
return;
|
||||
}
|
||||
const oldValue = changedProperties.get("condition") as
|
||||
| undefined
|
||||
| this["condition"];
|
||||
|
||||
// Fetch the manifest if we have a condition selected and the condition domain changed.
|
||||
// If no condition is selected, clear the manifest.
|
||||
if (this.condition?.condition) {
|
||||
const domain = getConditionDomain(this.condition.condition);
|
||||
|
||||
const oldDomain = getConditionDomain(oldValue?.condition || "");
|
||||
|
||||
if (domain !== oldDomain) {
|
||||
this._fetchManifest(domain);
|
||||
}
|
||||
} else {
|
||||
this._manifest = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const domain = getConditionDomain(this.condition.condition);
|
||||
const conditionName = getConditionObjectId(this.condition.condition);
|
||||
|
||||
const description = this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.description`
|
||||
);
|
||||
|
||||
const conditionDesc = this.description;
|
||||
|
||||
const shouldRenderDataYaml = !conditionDesc?.fields;
|
||||
|
||||
const hasOptional = Boolean(
|
||||
conditionDesc?.fields &&
|
||||
Object.values(conditionDesc.fields).some((field) =>
|
||||
showOptionalToggle(field)
|
||||
)
|
||||
);
|
||||
|
||||
return html`
|
||||
<div class="description">
|
||||
${description ? html`<p>${description}</p>` : nothing}
|
||||
${this._manifest
|
||||
? html`<a
|
||||
href=${this._manifest.is_built_in
|
||||
? documentationUrl(
|
||||
this.hass,
|
||||
`/integrations/${this._manifest.domain}`
|
||||
)
|
||||
: this._manifest.documentation}
|
||||
title=${this.hass.localize(
|
||||
"ui.components.service-control.integration_doc"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<ha-icon-button
|
||||
.path=${mdiHelpCircle}
|
||||
class="help-icon"
|
||||
></ha-icon-button>
|
||||
</a>`
|
||||
: nothing}
|
||||
</div>
|
||||
${conditionDesc && "target" in conditionDesc
|
||||
? html`<ha-settings-row narrow>
|
||||
${hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: nothing}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target"
|
||||
)}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
"ui.components.service-control.target_secondary"
|
||||
)}</span
|
||||
><ha-selector
|
||||
.hass=${this.hass}
|
||||
.selector=${this._targetSelector(conditionDesc.target)}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this.condition?.target}
|
||||
></ha-selector
|
||||
></ha-settings-row>`
|
||||
: nothing}
|
||||
${shouldRenderDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.service-control.action_data"
|
||||
)}
|
||||
.name=${"data"}
|
||||
.readOnly=${this.disabled}
|
||||
.defaultValue=${this.condition?.options}
|
||||
@value-changed=${this._dataChanged}
|
||||
></ha-yaml-editor>`
|
||||
: Object.entries(conditionDesc.fields).map(([fieldName, dataField]) =>
|
||||
this._renderField(
|
||||
fieldName,
|
||||
dataField,
|
||||
hasOptional,
|
||||
domain,
|
||||
conditionName
|
||||
)
|
||||
)}
|
||||
`;
|
||||
}
|
||||
|
||||
private _targetSelector = memoizeOne(
|
||||
(targetSelector: TargetSelector["target"] | null | undefined) =>
|
||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
||||
);
|
||||
|
||||
private _renderField = (
|
||||
fieldName: string,
|
||||
dataField: ConditionDescription["fields"][string],
|
||||
hasOptional: boolean,
|
||||
domain: string | undefined,
|
||||
conditionName: string | undefined
|
||||
) => {
|
||||
const selector = dataField?.selector ?? { text: null };
|
||||
|
||||
const showOptional = showOptionalToggle(dataField);
|
||||
|
||||
return dataField.selector
|
||||
? html`<ha-settings-row narrow>
|
||||
${!showOptional
|
||||
? hasOptional
|
||||
? html`<div slot="prefix" class="checkbox-spacer"></div>`
|
||||
: nothing
|
||||
: html`<ha-checkbox
|
||||
.key=${fieldName}
|
||||
.checked=${this._checkedKeys.has(fieldName) ||
|
||||
(this.condition?.options &&
|
||||
this.condition.options[fieldName] !== undefined)}
|
||||
.disabled=${this.disabled}
|
||||
@change=${this._checkboxChanged}
|
||||
slot="prefix"
|
||||
></ha-checkbox>`}
|
||||
<span slot="heading"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.name`
|
||||
) || conditionName}</span
|
||||
>
|
||||
<span slot="description"
|
||||
>${this.hass.localize(
|
||||
`component.${domain}.conditions.${conditionName}.fields.${fieldName}.description`
|
||||
)}</span
|
||||
>
|
||||
<ha-selector
|
||||
.disabled=${this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(fieldName) &&
|
||||
(!this.condition?.options ||
|
||||
this.condition.options[fieldName] === undefined))}
|
||||
.hass=${this.hass}
|
||||
.selector=${selector}
|
||||
.context=${this._generateContext(dataField)}
|
||||
.key=${fieldName}
|
||||
@value-changed=${this._dataChanged}
|
||||
.value=${this.condition?.options
|
||||
? this.condition.options[fieldName]
|
||||
: undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
></ha-selector>
|
||||
</ha-settings-row>`
|
||||
: nothing;
|
||||
};
|
||||
|
||||
private _generateContext(
|
||||
field: ConditionDescription["fields"][string]
|
||||
): Record<string, any> | undefined {
|
||||
if (!field.context) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const context = {};
|
||||
for (const [context_key, data_key] of Object.entries(field.context)) {
|
||||
context[context_key] =
|
||||
data_key === "target"
|
||||
? this.condition.target
|
||||
: this.condition.options?.[data_key];
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
private _dataChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
if (ev.detail.isValid === false) {
|
||||
// Don't clear an object selector that returns invalid YAML
|
||||
return;
|
||||
}
|
||||
const key = (ev.currentTarget as any).key;
|
||||
const value = ev.detail.value;
|
||||
if (
|
||||
this.condition?.options?.[key] === value ||
|
||||
((!this.condition?.options || !(key in this.condition.options)) &&
|
||||
(value === "" || value === undefined))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const options = { ...this.condition?.options, [key]: value };
|
||||
|
||||
if (
|
||||
value === "" ||
|
||||
value === undefined ||
|
||||
(typeof value === "object" && !Object.keys(value).length)
|
||||
) {
|
||||
delete options[key];
|
||||
}
|
||||
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
options,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _targetChanged(ev: CustomEvent): void {
|
||||
ev.stopPropagation();
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
target: ev.detail.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private _checkboxChanged(ev) {
|
||||
const checked = ev.currentTarget.checked;
|
||||
const key = ev.currentTarget.key;
|
||||
let options;
|
||||
|
||||
if (checked) {
|
||||
this._checkedKeys.add(key);
|
||||
const field =
|
||||
this.description &&
|
||||
Object.entries(this.description).find(([k, _value]) => k === key)?.[1];
|
||||
let defaultValue = field?.default;
|
||||
|
||||
if (
|
||||
defaultValue == null &&
|
||||
field?.selector &&
|
||||
"constant" in field.selector
|
||||
) {
|
||||
defaultValue = field.selector.constant?.value;
|
||||
}
|
||||
|
||||
if (
|
||||
defaultValue == null &&
|
||||
field?.selector &&
|
||||
"boolean" in field.selector
|
||||
) {
|
||||
defaultValue = false;
|
||||
}
|
||||
|
||||
if (defaultValue != null) {
|
||||
options = {
|
||||
...this.condition?.options,
|
||||
[key]: defaultValue,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
this._checkedKeys.delete(key);
|
||||
options = { ...this.condition?.options };
|
||||
delete options[key];
|
||||
}
|
||||
if (options) {
|
||||
fireEvent(this, "value-changed", {
|
||||
value: {
|
||||
...this.condition,
|
||||
options,
|
||||
},
|
||||
});
|
||||
}
|
||||
this.requestUpdate("_checkedKeys");
|
||||
}
|
||||
|
||||
private _localizeValueCallback = (key: string) => {
|
||||
if (!this.condition?.condition) {
|
||||
return "";
|
||||
}
|
||||
return this.hass.localize(
|
||||
`component.${computeDomain(this.condition.condition)}.selector.${key}`
|
||||
);
|
||||
};
|
||||
|
||||
private async _fetchManifest(integration: string) {
|
||||
this._manifest = undefined;
|
||||
try {
|
||||
this._manifest = await fetchIntegrationManifest(this.hass, integration);
|
||||
} catch (_err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Unable to fetch integration manifest for ${integration}`);
|
||||
// Ignore if loading manifest fails. Probably bad JSON in manifest
|
||||
}
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-settings-row {
|
||||
padding: 0 var(--ha-space-4);
|
||||
}
|
||||
ha-settings-row[narrow] {
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-settings-row {
|
||||
--settings-row-content-width: 100%;
|
||||
--settings-row-prefix-display: contents;
|
||||
border-top: var(
|
||||
--service-control-items-border-top,
|
||||
1px solid var(--divider-color)
|
||||
);
|
||||
}
|
||||
ha-service-picker,
|
||||
ha-entity-picker,
|
||||
ha-yaml-editor {
|
||||
display: block;
|
||||
margin: 0 var(--ha-space-4);
|
||||
}
|
||||
ha-yaml-editor {
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
p {
|
||||
margin: 0 var(--ha-space-4);
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
:host([hide-picker]) p {
|
||||
padding-top: 0;
|
||||
}
|
||||
.checkbox-spacer {
|
||||
width: 32px;
|
||||
}
|
||||
ha-checkbox {
|
||||
margin-left: calc(var(--ha-space-4) * -1);
|
||||
margin-inline-start: calc(var(--ha-space-4) * -1);
|
||||
margin-inline-end: initial;
|
||||
}
|
||||
.help-icon {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
.description {
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 2px;
|
||||
padding-inline-end: 2px;
|
||||
padding-inline-start: initial;
|
||||
}
|
||||
.description p {
|
||||
direction: ltr;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-automation-condition-platform": HaPlatformCondition;
|
||||
}
|
||||
}
|
||||
@@ -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,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,86 +105,63 @@ export default class HaAutomationSidebarCard extends LitElement {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-color: var(--primary-color);
|
||||
border-width: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
static get styles() {
|
||||
return [
|
||||
...super.styles,
|
||||
haStyleScrollbar,
|
||||
css`
|
||||
ha-card {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border-color: var(--primary-color);
|
||||
border-width: 2px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
ha-card.mobile {
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
@media all and (max-width: 870px) {
|
||||
ha-card.mobile {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
ha-card.mobile {
|
||||
border-bottom-right-radius: var(--ha-border-radius-square);
|
||||
border-bottom-left-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
}
|
||||
ha-dialog-header {
|
||||
border-radius: var(--ha-card-border-radius);
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
position: relative;
|
||||
background-color: var(
|
||||
--ha-dialog-surface-background,
|
||||
var(--mdc-theme-surface, #fff)
|
||||
);
|
||||
}
|
||||
|
||||
ha-dialog-header.scrolled {
|
||||
box-shadow: var(--bar-box-shadow);
|
||||
}
|
||||
.card-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
|
||||
}
|
||||
|
||||
.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-top {
|
||||
top: var(--ha-space-17);
|
||||
}
|
||||
|
||||
.fade.scrollable {
|
||||
box-shadow: var(--bar-box-shadow);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
margin-top: 0;
|
||||
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);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
padding-bottom: 42px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@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}
|
||||
|
||||
@@ -12,7 +12,7 @@ import "../../../lovelace/editor/dashboard-strategy-editor/hui-dashboard-strateg
|
||||
import type { LovelaceDashboardConfigureStrategyDialogParams } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-configure-strategy")
|
||||
export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
export class DialogLovelaceDashboardConfigureStrategy extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@state() private _params?: LovelaceDashboardConfigureStrategyDialogParams;
|
||||
@@ -97,6 +97,6 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-lovelace-dashboard-configure-strategy": DialogLovelaceDashboardDetail;
|
||||
"dialog-lovelace-dashboard-configure-strategy": DialogLovelaceDashboardConfigureStrategy;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,16 +8,13 @@ import "../../../../components/ha-button";
|
||||
import { createCloseHeading } from "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../components/ha-form/types";
|
||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||
import type {
|
||||
LovelaceDashboard,
|
||||
LovelaceDashboardCreateParams,
|
||||
LovelaceDashboardMutableParams,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import { haStyleDialog } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { showConfirmationDialog } from "../../../lovelace/custom-card-helpers";
|
||||
import type { LovelaceDashboardDetailsDialogParams } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
@customElement("dialog-lovelace-dashboard-detail")
|
||||
@@ -61,9 +58,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
if (!this._params || !this._data) {
|
||||
return nothing;
|
||||
}
|
||||
const defaultPanelUrlPath =
|
||||
this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
|
||||
const titleInvalid = !this._data.title || !this._data.title.trim();
|
||||
const isLovelaceDashboard = this._params.urlPath === "lovelace";
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -88,9 +85,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_yaml"
|
||||
)
|
||||
: this._params.urlPath === "lovelace"
|
||||
: isLovelaceDashboard
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_default"
|
||||
"ui.panel.config.lovelace.dashboards.cant_edit_lovelace"
|
||||
)
|
||||
: html`
|
||||
<ha-form
|
||||
@@ -119,24 +116,9 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
)}
|
||||
</ha-button>
|
||||
`
|
||||
: ""}
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this._toggleDefault}
|
||||
.disabled=${this._params.urlPath === "lovelace" &&
|
||||
defaultPanelUrlPath === "lovelace"}
|
||||
>
|
||||
${this._params.urlPath === defaultPanelUrlPath
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.remove_default"
|
||||
)
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default"
|
||||
)}
|
||||
</ha-button>
|
||||
: nothing}
|
||||
`
|
||||
: ""}
|
||||
: nothing}
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
@click=${this._updateDashboard}
|
||||
@@ -254,40 +236,6 @@ export class DialogLovelaceDashboardDetail extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private async _toggleDefault() {
|
||||
const urlPath = this._params?.urlPath;
|
||||
if (!urlPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
const defaultPanel = this.hass.systemData?.default_panel || DEFAULT_PANEL;
|
||||
// Add warning dialog to saying that this will change the default dashboard for all users
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_title"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
urlPath === defaultPanel
|
||||
? "ui.panel.config.lovelace.dashboards.detail.remove_default_confirm_text"
|
||||
: "ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
default_panel: urlPath === defaultPanel ? undefined : urlPath,
|
||||
});
|
||||
}
|
||||
|
||||
private async _updateDashboard() {
|
||||
if (this._params?.urlPath && !this._params.dashboard?.id) {
|
||||
this.closeDialog();
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {
|
||||
mdiCheck,
|
||||
mdiCheckCircleOutline,
|
||||
mdiDelete,
|
||||
mdiDotsVertical,
|
||||
mdiHomeCircleOutline,
|
||||
mdiHomeEdit,
|
||||
mdiPencil,
|
||||
mdiPlus,
|
||||
} from "@mdi/js";
|
||||
@@ -10,7 +11,6 @@ import type { PropertyValues } from "lit";
|
||||
import { LitElement, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoize from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../../common/config/is_component_loaded";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import { navigate } from "../../../../common/navigate";
|
||||
import { stringCompare } from "../../../../common/string/compare";
|
||||
@@ -29,6 +29,7 @@ import "../../../../components/ha-md-button-menu";
|
||||
import "../../../../components/ha-md-list-item";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/ha-tooltip";
|
||||
import { saveFrontendSystemData } from "../../../../data/frontend";
|
||||
import type { LovelacePanelConfig } from "../../../../data/lovelace";
|
||||
import type { LovelaceRawConfig } from "../../../../data/lovelace/config/types";
|
||||
import {
|
||||
@@ -45,7 +46,11 @@ import {
|
||||
fetchDashboards,
|
||||
updateDashboard,
|
||||
} from "../../../../data/lovelace/dashboard";
|
||||
import { DEFAULT_PANEL } from "../../../../data/panel";
|
||||
import {
|
||||
DEFAULT_PANEL,
|
||||
getPanelIcon,
|
||||
getPanelTitle,
|
||||
} from "../../../../data/panel";
|
||||
import { showConfirmationDialog } from "../../../../dialogs/generic/show-dialog-box";
|
||||
import "../../../../layouts/hass-loading-screen";
|
||||
import "../../../../layouts/hass-tabs-subpage-data-table";
|
||||
@@ -56,12 +61,21 @@ import { lovelaceTabs } from "../ha-config-lovelace";
|
||||
import { showDashboardConfigureStrategyDialog } from "./show-dialog-lovelace-dashboard-configure-strategy";
|
||||
import { showDashboardDetailDialog } from "./show-dialog-lovelace-dashboard-detail";
|
||||
|
||||
export const PANEL_DASHBOARDS = [
|
||||
"home",
|
||||
"light",
|
||||
"security",
|
||||
"climate",
|
||||
"energy",
|
||||
] as string[];
|
||||
|
||||
type DataTableItem = Pick<
|
||||
LovelaceDashboard,
|
||||
"icon" | "title" | "show_in_sidebar" | "require_admin" | "mode" | "url_path"
|
||||
> & {
|
||||
default: boolean;
|
||||
filename: string;
|
||||
localized_type: string;
|
||||
type: string;
|
||||
};
|
||||
|
||||
@@ -112,7 +126,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
state: false,
|
||||
subscribe: false,
|
||||
})
|
||||
private _activeGrouping?: string = "type";
|
||||
private _activeGrouping?: string = "localized_type";
|
||||
|
||||
@storage({
|
||||
key: "lovelace-dashboards-table-collapsed",
|
||||
@@ -167,7 +181,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
<ha-svg-icon
|
||||
.id="default-icon-${dashboard.title}"
|
||||
style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);"
|
||||
.path=${mdiCheckCircleOutline}
|
||||
.path=${mdiHomeCircleOutline}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip
|
||||
.for="default-icon-${dashboard.title}"
|
||||
@@ -183,7 +197,7 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
},
|
||||
};
|
||||
|
||||
columns.type = {
|
||||
columns.localized_type = {
|
||||
title: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.headers.type"
|
||||
),
|
||||
@@ -253,7 +267,15 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
.hass=${this.hass}
|
||||
narrow
|
||||
.items=${[
|
||||
...(this._canEdit(dashboard.url_path)
|
||||
{
|
||||
path: mdiHomeEdit,
|
||||
label: localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.set_as_default"
|
||||
),
|
||||
action: () => this._handleSetAsDefault(dashboard),
|
||||
disabled: dashboard.default,
|
||||
},
|
||||
...(dashboard.type === "user_created"
|
||||
? [
|
||||
{
|
||||
path: mdiPencil,
|
||||
@@ -262,10 +284,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
),
|
||||
action: () => this._handleEdit(dashboard),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(this._canDelete(dashboard.url_path)
|
||||
? [
|
||||
{
|
||||
label: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.picker.delete"
|
||||
@@ -288,92 +306,43 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
|
||||
private _getItems = memoize(
|
||||
(dashboards: LovelaceDashboard[], defaultUrlPath: string | null) => {
|
||||
const defaultMode = (
|
||||
this.hass.panels?.lovelace?.config as LovelacePanelConfig
|
||||
).mode;
|
||||
const mode = (this.hass.panels?.lovelace?.config as LovelacePanelConfig)
|
||||
.mode;
|
||||
const isDefault = defaultUrlPath === "lovelace";
|
||||
const result: DataTableItem[] = [
|
||||
{
|
||||
icon: "mdi:view-dashboard",
|
||||
title: this.hass.localize("panel.states"),
|
||||
default: isDefault,
|
||||
show_in_sidebar: isDefault,
|
||||
show_in_sidebar: true,
|
||||
require_admin: false,
|
||||
url_path: "lovelace",
|
||||
mode: defaultMode,
|
||||
filename: defaultMode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||
type: this._localizeType("built_in"),
|
||||
mode: mode,
|
||||
filename: mode === "yaml" ? "ui-lovelace.yaml" : "",
|
||||
type: "built_in",
|
||||
localized_type: this._localizeType("built_in"),
|
||||
},
|
||||
];
|
||||
if (isComponentLoaded(this.hass, "energy")) {
|
||||
result.push({
|
||||
icon: "mdi:lightning-bolt",
|
||||
title: this.hass.localize(`ui.panel.config.dashboard.energy.main`),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "energy",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.light) {
|
||||
result.push({
|
||||
icon: this.hass.panels.light.icon || "mdi:lamps",
|
||||
title: this.hass.localize("panel.light"),
|
||||
PANEL_DASHBOARDS.forEach((panel) => {
|
||||
const panelInfo = this.hass.panels[panel];
|
||||
if (!panel) {
|
||||
return;
|
||||
}
|
||||
const item: DataTableItem = {
|
||||
icon: getPanelIcon(panelInfo),
|
||||
title: getPanelTitle(this.hass, panelInfo) || panelInfo.url_path,
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "light",
|
||||
url_path: panelInfo.url_path,
|
||||
filename: "",
|
||||
default: false,
|
||||
default: defaultUrlPath === panelInfo.url_path,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.security) {
|
||||
result.push({
|
||||
icon: this.hass.panels.security.icon || "mdi:security",
|
||||
title: this.hass.localize("panel.security"),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "security",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.climate) {
|
||||
result.push({
|
||||
icon: this.hass.panels.climate.icon || "mdi:home-thermometer",
|
||||
title: this.hass.localize("panel.climate"),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "climate",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
|
||||
if (this.hass.panels.home) {
|
||||
result.push({
|
||||
icon: this.hass.panels.home.icon || "mdi:home",
|
||||
title: this.hass.localize("panel.home"),
|
||||
show_in_sidebar: true,
|
||||
mode: "storage",
|
||||
url_path: "home",
|
||||
filename: "",
|
||||
default: false,
|
||||
require_admin: false,
|
||||
type: this._localizeType("built_in"),
|
||||
});
|
||||
}
|
||||
type: "built_in",
|
||||
localized_type: this._localizeType("built_in"),
|
||||
};
|
||||
result.push(item);
|
||||
});
|
||||
|
||||
result.push(
|
||||
...dashboards
|
||||
@@ -386,7 +355,8 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
filename: "",
|
||||
...dashboard,
|
||||
default: defaultUrlPath === dashboard.url_path,
|
||||
type: this._localizeType("user_created"),
|
||||
type: "user_created",
|
||||
localized_type: this._localizeType("user_created"),
|
||||
}) satisfies DataTableItem
|
||||
)
|
||||
);
|
||||
@@ -486,20 +456,32 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
this._openDetailDialog(dashboard, urlPath);
|
||||
}
|
||||
|
||||
private _canDelete(urlPath: string) {
|
||||
return ![
|
||||
"lovelace",
|
||||
"energy",
|
||||
"light",
|
||||
"security",
|
||||
"climate",
|
||||
"home",
|
||||
].includes(urlPath);
|
||||
}
|
||||
private _handleSetAsDefault = async (item: DataTableItem) => {
|
||||
if (item.default) {
|
||||
return;
|
||||
}
|
||||
|
||||
private _canEdit(urlPath: string) {
|
||||
return !["light", "security", "climate", "home"].includes(urlPath);
|
||||
}
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default_confirm_title"
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.lovelace.dashboards.detail.set_default_confirm_text"
|
||||
),
|
||||
confirmText: this.hass.localize("ui.common.ok"),
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
destructive: false,
|
||||
});
|
||||
|
||||
if (!confirm) {
|
||||
return;
|
||||
}
|
||||
|
||||
await saveFrontendSystemData(this.hass.connection, "core", {
|
||||
...this.hass.systemData,
|
||||
default_panel: item.url_path,
|
||||
});
|
||||
};
|
||||
|
||||
private _handleDelete = async (item: DataTableItem) => {
|
||||
const dashboard = this._dashboards.find(
|
||||
@@ -581,10 +563,6 @@ export class HaConfigLovelaceDashboards extends LitElement {
|
||||
private async _deleteDashboard(
|
||||
dashboard: LovelaceDashboard
|
||||
): Promise<boolean> {
|
||||
if (!this._canDelete(dashboard.url_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const confirm = await showConfirmationDialog(this, {
|
||||
title: this.hass!.localize(
|
||||
"ui.panel.config.lovelace.dashboards.confirm_delete_title",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,60 +1,60 @@
|
||||
import { mdiDownload, mdiPencil } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { mdiPencil, mdiDownload } from "@mdi/js";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-menu-button";
|
||||
import { goBack, navigate } from "../../common/navigate";
|
||||
import "../../components/ha-alert";
|
||||
import "../../components/ha-icon-button-arrow-prev";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-menu-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import "../../components/ha-alert";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../lovelace/components/hui-energy-period-selector";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
import { goBack, navigate } from "../../common/navigate";
|
||||
import type {
|
||||
BatterySourceTypeEnergyPreference,
|
||||
DeviceConsumptionEnergyPreference,
|
||||
EnergyPreferences,
|
||||
GasSourceTypeEnergyPreference,
|
||||
GridSourceTypeEnergyPreference,
|
||||
SolarSourceTypeEnergyPreference,
|
||||
BatterySourceTypeEnergyPreference,
|
||||
GasSourceTypeEnergyPreference,
|
||||
WaterSourceTypeEnergyPreference,
|
||||
DeviceConsumptionEnergyPreference,
|
||||
EnergyCollection,
|
||||
} from "../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
getEnergyDataCollection,
|
||||
getSummedData,
|
||||
} from "../../data/energy";
|
||||
import { fileDownload } from "../../util/file_download";
|
||||
import type { LovelaceConfig } from "../../data/lovelace/config/types";
|
||||
import type { LovelaceViewConfig } from "../../data/lovelace/config/view";
|
||||
import type { StatisticValue } from "../../data/recorder";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { fileDownload } from "../../util/file_download";
|
||||
import "../lovelace/components/hui-energy-period-selector";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
|
||||
export const DEFAULT_ENERGY_COLLECTION_KEY = "energy_dashboard";
|
||||
|
||||
const ENERGY_LOVELACE_CONFIG: LovelaceConfig = {
|
||||
views: [
|
||||
{
|
||||
strategy: {
|
||||
type: "energy-overview",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
},
|
||||
{
|
||||
strategy: {
|
||||
type: "energy-electricity",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
path: "electricity",
|
||||
},
|
||||
{
|
||||
type: "panel",
|
||||
path: "setup",
|
||||
cards: [{ type: "custom:energy-setup-wizard-card" }],
|
||||
},
|
||||
],
|
||||
const OVERVIEW_VIEW = {
|
||||
strategy: {
|
||||
type: "energy-overview",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
};
|
||||
|
||||
const ELECTRICITY_VIEW = {
|
||||
back_path: "/energy",
|
||||
path: "electricity",
|
||||
strategy: {
|
||||
type: "energy-electricity",
|
||||
collection_key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
},
|
||||
} as LovelaceViewConfig;
|
||||
|
||||
const WIZARD_VIEW = {
|
||||
type: "panel",
|
||||
path: "setup",
|
||||
cards: [{ type: "custom:energy-setup-wizard-card" }],
|
||||
};
|
||||
|
||||
@customElement("ha-panel-energy")
|
||||
@@ -74,7 +74,8 @@ class PanelEnergy extends LitElement {
|
||||
prefix: string;
|
||||
};
|
||||
|
||||
private _energyCollection?: EnergyCollection;
|
||||
@state()
|
||||
private _config?: LovelaceConfig;
|
||||
|
||||
get _viewPath(): string | undefined {
|
||||
const viewPath: string | undefined = this.route!.path.split("/")[1];
|
||||
@@ -83,7 +84,7 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this._loadPrefs();
|
||||
this._loadLovelaceConfig();
|
||||
}
|
||||
|
||||
public async willUpdate(changedProps: PropertyValues) {
|
||||
@@ -101,32 +102,15 @@ class PanelEnergy extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
private async _loadPrefs() {
|
||||
if (this._viewPath === "setup") {
|
||||
await import("./cards/energy-setup-wizard-card");
|
||||
} else {
|
||||
this._energyCollection = getEnergyDataCollection(this.hass, {
|
||||
key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
});
|
||||
try {
|
||||
// Have to manually refresh here as we don't want to subscribe yet
|
||||
await this._energyCollection.refresh();
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
navigate("/energy/setup");
|
||||
}
|
||||
this._error = err.message;
|
||||
return;
|
||||
}
|
||||
const prefs = this._energyCollection.prefs!;
|
||||
if (
|
||||
prefs.device_consumption.length === 0 &&
|
||||
prefs.energy_sources.length === 0
|
||||
) {
|
||||
// No energy sources available, start from scratch
|
||||
navigate("/energy/setup");
|
||||
}
|
||||
private async _loadLovelaceConfig() {
|
||||
try {
|
||||
this._config = undefined;
|
||||
this._config = await this._generateLovelaceConfig();
|
||||
} catch (err) {
|
||||
this._error = (err as Error).message;
|
||||
}
|
||||
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private _back(ev) {
|
||||
@@ -135,25 +119,22 @@ class PanelEnergy extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this._energyCollection?.prefs) {
|
||||
if (!this._config && !this._error) {
|
||||
// Still loading
|
||||
return html`<div class="centered">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</div>`;
|
||||
return html`
|
||||
<div class="centered">
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const { prefs } = this._energyCollection;
|
||||
const isSingleView = prefs.energy_sources.every((source) =>
|
||||
["grid", "solar", "battery"].includes(source.type)
|
||||
);
|
||||
let viewPath = this._viewPath;
|
||||
if (isSingleView) {
|
||||
// if only electricity sources, show electricity view directly
|
||||
viewPath = "electricity";
|
||||
}
|
||||
const viewIndex = Math.max(
|
||||
ENERGY_LOVELACE_CONFIG.views.findIndex((view) => view.path === viewPath),
|
||||
0
|
||||
);
|
||||
const isSingleView = this._config?.views.length === 1;
|
||||
const viewPath = this._viewPath;
|
||||
const viewIndex = this._config
|
||||
? Math.max(
|
||||
this._config.views.findIndex((view) => view.path === viewPath),
|
||||
0
|
||||
)
|
||||
: 0;
|
||||
const showBack =
|
||||
this._searchParms.has("historyBack") || (!isSingleView && viewIndex > 0);
|
||||
|
||||
@@ -229,10 +210,56 @@ class PanelEnergy extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _fetchEnergyPrefs = async (): Promise<
|
||||
EnergyPreferences | undefined
|
||||
> => {
|
||||
const collection = getEnergyDataCollection(this.hass, {
|
||||
key: DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
});
|
||||
try {
|
||||
await collection.refresh();
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_found") {
|
||||
return undefined;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return collection.prefs;
|
||||
};
|
||||
|
||||
private async _generateLovelaceConfig(): Promise<LovelaceConfig> {
|
||||
const prefs = await this._fetchEnergyPrefs();
|
||||
if (
|
||||
!prefs ||
|
||||
(prefs.device_consumption.length === 0 &&
|
||||
prefs.energy_sources.length === 0)
|
||||
) {
|
||||
await import("./cards/energy-setup-wizard-card");
|
||||
return {
|
||||
views: [WIZARD_VIEW],
|
||||
};
|
||||
}
|
||||
|
||||
const isElectricityOnly = prefs.energy_sources.every((source) =>
|
||||
["grid", "solar", "battery"].includes(source.type)
|
||||
);
|
||||
if (isElectricityOnly) {
|
||||
return {
|
||||
views: [ELECTRICITY_VIEW],
|
||||
};
|
||||
}
|
||||
return {
|
||||
views: [OVERVIEW_VIEW, ELECTRICITY_VIEW],
|
||||
};
|
||||
}
|
||||
|
||||
private _setLovelace() {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
this._lovelace = {
|
||||
config: ENERGY_LOVELACE_CONFIG,
|
||||
rawConfig: ENERGY_LOVELACE_CONFIG,
|
||||
config: this._config,
|
||||
rawConfig: this._config,
|
||||
editMode: false,
|
||||
urlPath: "energy",
|
||||
mode: "generated",
|
||||
@@ -252,7 +279,9 @@ class PanelEnergy extends LitElement {
|
||||
|
||||
private async _dumpCSV(ev) {
|
||||
ev.stopPropagation();
|
||||
const energyData = this._energyCollection!;
|
||||
const energyData = getEnergyDataCollection(this.hass, {
|
||||
key: "energy_dashboard",
|
||||
});
|
||||
|
||||
if (!energyData.prefs || !energyData.state.stats) {
|
||||
return;
|
||||
@@ -549,12 +578,7 @@ class PanelEnergy extends LitElement {
|
||||
}
|
||||
|
||||
private _reloadView() {
|
||||
// Force strategy to be re-run by making a copy of the view
|
||||
const config = this._lovelace!.config;
|
||||
this._lovelace = {
|
||||
...this._lovelace!,
|
||||
config: { ...config, views: config.views.map((view) => ({ ...view })) },
|
||||
};
|
||||
this._loadLovelaceConfig();
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -46,7 +46,7 @@ const STRATEGIES: Record<LovelaceStrategyConfigType, Record<string, any>> = {
|
||||
iframe: () => import("./iframe/iframe-view-strategy"),
|
||||
area: () => import("./areas/area-view-strategy"),
|
||||
"areas-overview": () => import("./areas/areas-overview-view-strategy"),
|
||||
"home-main": () => import("./home/home-main-view-strategy"),
|
||||
"home-overview": () => import("./home/home-overview-view-strategy"),
|
||||
"home-media-players": () =>
|
||||
import("./home/home-media-players-view-strategy"),
|
||||
"home-area": () => import("./home/home-area-view-strategy"),
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
HOME_SUMMARIES_ICONS,
|
||||
} from "./helpers/home-summaries";
|
||||
import type { HomeAreaViewStrategyConfig } from "./home-area-view-strategy";
|
||||
import type { HomeMainViewStrategyConfig } from "./home-main-view-strategy";
|
||||
import type { HomeOverviewViewStrategyConfig } from "./home-overview-view-strategy";
|
||||
|
||||
export interface HomeDashboardStrategyConfig {
|
||||
type: "home";
|
||||
@@ -75,11 +75,11 @@ export class HomeDashboardStrategy extends ReactiveElement {
|
||||
views: [
|
||||
{
|
||||
icon: "mdi:home",
|
||||
path: "home",
|
||||
path: "overview",
|
||||
strategy: {
|
||||
type: "home-main",
|
||||
type: "home-overview",
|
||||
favorite_entities: config.favorite_entities,
|
||||
} satisfies HomeMainViewStrategyConfig,
|
||||
} satisfies HomeOverviewViewStrategyConfig,
|
||||
},
|
||||
...areaViews,
|
||||
mediaPlayersView,
|
||||
|
||||
@@ -26,8 +26,8 @@ import type { CommonControlSectionStrategyConfig } from "../usage_prediction/com
|
||||
import { getAreasFloorHierarchy } from "../../../../common/areas/areas-floor-hierarchy";
|
||||
import { HOME_SUMMARIES_FILTERS } from "./helpers/home-summaries";
|
||||
|
||||
export interface HomeMainViewStrategyConfig {
|
||||
type: "home-main";
|
||||
export interface HomeOverviewViewStrategyConfig {
|
||||
type: "home-overview";
|
||||
favorite_entities?: string[];
|
||||
}
|
||||
|
||||
@@ -57,10 +57,10 @@ const computeAreaCard = (
|
||||
};
|
||||
};
|
||||
|
||||
@customElement("home-main-view-strategy")
|
||||
export class HomeMainViewStrategy extends ReactiveElement {
|
||||
@customElement("home-overview-view-strategy")
|
||||
export class HomeOverviewViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
config: HomeMainViewStrategyConfig,
|
||||
config: HomeOverviewViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
@@ -325,6 +325,6 @@ export class HomeMainViewStrategy extends ReactiveElement {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"home-main-view-strategy": HomeMainViewStrategy;
|
||||
"home-overview-view-strategy": HomeOverviewViewStrategy;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../components/ha-divider";
|
||||
import "../../components/ha-list-item";
|
||||
import "../../components/ha-select";
|
||||
import "../../components/ha-settings-row";
|
||||
import { saveFrontendUserData } from "../../data/frontend";
|
||||
import type { LovelaceDashboard } from "../../data/lovelace/dashboard";
|
||||
import { fetchDashboards } from "../../data/lovelace/dashboard";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { saveFrontendUserData } from "../../data/frontend";
|
||||
import { getPanelTitle } from "../../data/panel";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
import { PANEL_DASHBOARDS } from "../config/lovelace/dashboards/ha-config-lovelace-dashboards";
|
||||
|
||||
const USE_SYSTEM_VALUE = "___use_system___";
|
||||
|
||||
@@ -47,12 +50,24 @@ class HaPickDashboardRow extends LitElement {
|
||||
<ha-list-item .value=${USE_SYSTEM_VALUE}>
|
||||
${this.hass.localize("ui.panel.profile.dashboard.system")}
|
||||
</ha-list-item>
|
||||
<ha-divider></ha-divider>
|
||||
<ha-list-item value="lovelace">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.lovelace")}
|
||||
</ha-list-item>
|
||||
<ha-list-item value="home">
|
||||
${this.hass.localize("ui.panel.profile.dashboard.home")}
|
||||
</ha-list-item>
|
||||
${PANEL_DASHBOARDS.map((panel) => {
|
||||
const panelInfo = this.hass.panels[panel] as
|
||||
| PanelInfo
|
||||
| undefined;
|
||||
if (!panelInfo) {
|
||||
return nothing;
|
||||
}
|
||||
return html`
|
||||
<ha-list-item value=${panelInfo.url_path}>
|
||||
${getPanelTitle(this.hass, panelInfo)}
|
||||
</ha-list-item>
|
||||
`;
|
||||
})}
|
||||
<ha-divider></ha-divider>
|
||||
${this._dashboards.map((dashboard) => {
|
||||
if (!this.hass.user!.is_admin && dashboard.require_admin) {
|
||||
return "";
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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": {
|
||||
@@ -7158,6 +7159,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"
|
||||
@@ -7302,6 +7304,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",
|
||||
|
||||
Reference in New Issue
Block a user