Compare commits

..

73 Commits

Author SHA1 Message Date
Wendelin
c74d59dfc6 Use feature flag 2025-11-25 16:56:53 +01:00
Wendelin
6c52b2252e Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-element-by-target 2025-11-25 16:30:09 +01:00
Wendelin
e7edb1ba80 Review simplify render 2025-11-25 15:26:26 +01:00
Wendelin
161ccc752c Review 2025-11-25 15:20:38 +01:00
Jan-Philipp Benecke
fe50c1212a Add undo/redo functionality to dashboard editor (#27259)
* Add undo/redo functionality to dashboard editor

* Use controller and move toast to undo stack

* Store location and navigate to view

* Await and catch errors

* Process code review
2025-11-25 16:09:13 +02:00
Petar Petrov
c01fbf5f47 Revert "Use entity name for activity/logbook renderer" (#28098) 2025-11-25 13:24:51 +01:00
Wendelin
1a6d1580a2 Make show more, more visible 2025-11-25 12:41:42 +01:00
Wendelin
5fde08bc47 Fix scroll to 2025-11-25 12:35:33 +01:00
Wendelin
bf36d4477e fix services name 2025-11-25 12:19:01 +01:00
Aidan Timson
5c8da28b61 Standardise scrollable area fade (#28074)
* Extract fade CSS into shared file

* Switch to linear gradient instead of box shadow

* Refactor

* Refactor

* Scrollbar

* Fix

* Create mixin

* Move styles into mixin

* Dont replace mixin styles

* Reuse bar-box-shadow

* Move position
2025-11-25 13:04:08 +02:00
Rahul Harpal
bbb3c0208b Remove defaultHidden property from area configuration in automation, scene, and script pickers (#28096) 2025-11-25 10:55:12 +00:00
Wendelin
2a6e48bc23 fix styles 2025-11-25 11:54:39 +01:00
Wendelin
f9f9622873 Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-element-by-target 2025-11-25 11:48:14 +01:00
Aidan Timson
82a3db39fe Use entity name for activity/logbook renderer (#28083) 2025-11-25 12:46:57 +02:00
Paul Bottein
254857b53f Improve sidebar and dashboard management logic (#28019)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2025-11-25 10:39:07 +00:00
Simon Lamon
1648be6b83 Rename DialogLovelaceDashboardConfigureStrategy (#28092) 2025-11-25 12:22:18 +02:00
Jan Layola
c4c774c217 Add total consumption to energy usage graph card (#28086)
* Add total consumption to energy usage graph card

* Add "used" suffix to energy usage graph card total

* Apply code review feedback

* Update src/panels/lovelace/cards/energy/hui-energy-usage-graph-card.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-25 10:18:29 +00:00
Bram Kragten
6a0aab2088 Add dynamic condition support (#28058)
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2025-11-25 11:17:53 +01:00
Wendelin
f89caaf1a5 Fix possible undefined entity 2025-11-25 11:03:56 +01:00
Wendelin
a3f0e3507b extract render items to own element 2025-11-25 10:57:51 +01:00
Wendelin
7ac34f424f Search into a external component 2025-11-25 08:59:27 +01:00
Wendelin
e041658ca1 Add info icon 2025-11-24 15:13:34 +01:00
Wendelin
d410363b81 Group target results by domain 2025-11-24 13:03:51 +01:00
Wendelin
fa58b07801 use stringCompare instead of localecompare 2025-11-24 13:02:36 +01:00
Wendelin
0b89f7af12 Fix SingleHassServiceTarget 2025-11-24 11:08:26 +01:00
Wendelin
cb7f35979a Remove export from getAreasAndFloorsItems 2025-11-24 11:02:07 +01:00
Wendelin
1dbdad5605 Fix types 2025-11-24 10:54:54 +01:00
Wendelin
75a127d453 Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-element-by-target 2025-11-24 10:45:12 +01:00
Wendelin
5c15354302 fix getAreasNestedInFloors 2025-11-21 13:43:59 +01:00
Wendelin
fedcf85264 Add keyboard shortcuts 2025-11-21 13:37:05 +01:00
Wendelin
4b9a716df7 Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-element-by-target 2025-11-21 13:01:37 +01:00
Wendelin
e319d116dc add code regions 2025-11-21 12:21:08 +01:00
Wendelin
c8b1fd074c TARGET_SEPARATOR to data 2025-11-21 12:06:33 +01:00
Wendelin
ec9bf7dd94 Fix item target 2025-11-21 12:04:36 +01:00
Wendelin
6d01949ff5 Add target to items 2025-11-21 12:03:25 +01:00
Wendelin
8a8d7dc77a add regions 2025-11-21 11:54:55 +01:00
Wendelin
5fc629f5c5 optimize render code 2025-11-21 11:48:42 +01:00
Wendelin
d070754bb9 Fix load conditions 2025-11-21 08:56:25 +01:00
Wendelin
a2a0f900a8 Add action service lists 2025-11-20 16:19:31 +01:00
Wendelin
b18d3bff21 fix narrow title and subtitle 2025-11-20 16:05:18 +01:00
Wendelin
b06b51d03d Fix mobile scroll 2025-11-20 15:50:51 +01:00
Wendelin
022343c113 Add memoize rendering 2025-11-20 15:30:10 +01:00
Wendelin
5ca10f4a38 Fix reset after search select 2025-11-20 15:12:04 +01:00
Wendelin
33af5b6a08 Fix domain tree items 2025-11-20 14:54:56 +01:00
Wendelin
0c89415c1c Fix font weight 2025-11-20 14:50:49 +01:00
Wendelin
bd6b2c685c Rebuild tree with unassigned 2025-11-20 14:33:32 +01:00
Wendelin
cdb37edc5f add sub unassigned 2025-11-19 17:47:37 +01:00
Wendelin
d68b1571ae Rearrange unassigned 2025-11-19 16:06:18 +01:00
Wendelin
a274727713 Update webawesome 2025-11-19 16:04:36 +01:00
Wendelin
58a1aac131 remove some usage of hass 2025-11-19 15:39:28 +01:00
Wendelin
4cdbda25ed Increase dialog size 2025-11-19 14:48:24 +01:00
Wendelin
618c6e1ef2 change ha-wa-dialog large width 2025-11-19 14:47:53 +01:00
Wendelin
10b308b906 Use core triggers for target 2025-11-19 14:31:11 +01:00
Wendelin
563c6f0fbf Load tree via frontend 2025-11-19 11:58:49 +01:00
Wendelin
1dc7f0ab73 Fix scroll styles 2025-11-19 09:31:57 +01:00
Wendelin
5c8954b714 Fix search 2025-11-19 09:29:52 +01:00
Wendelin
e506e4b082 Add search 2025-11-18 17:27:45 +01:00
Wendelin
9628d69998 Fix sort 2025-11-18 09:49:43 +01:00
Wendelin
350d8e7a49 remove todo 2025-11-18 09:21:40 +01:00
Wendelin
61a5b607e0 Fix show more button 2025-11-18 09:19:51 +01:00
Wendelin
2f3a2b8418 Add unassigned section 2025-11-17 15:57:19 +01:00
Wendelin
12c1e4eec4 Add mobile max height 50percent 2025-11-17 14:25:16 +01:00
Wendelin
b704b621f2 Fix hidden addFromTarget 2025-11-17 10:41:40 +01:00
Wendelin
0eb993a8df Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-element-by-target 2025-11-17 09:55:31 +01:00
Wendelin
cda97766ac also hide narrow target selector when entity is active 2025-11-17 09:47:16 +01:00
Wendelin
e6936a9294 Add mobile view 2025-11-13 15:56:14 +01:00
Wendelin
5ad73287a2 Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-element-by-target 2025-11-13 09:07:37 +01:00
Wendelin
591b464508 Add devices and entities to tree 2025-11-12 17:00:26 +01:00
Wendelin
acf963d38c Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-element-by-target 2025-11-12 15:30:41 +01:00
Wendelin
68f383c293 Use areas and floors in add from target picker 2025-11-12 08:22:35 +01:00
Wendelin
0a25d8106c Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-element-by-target 2025-11-11 09:11:48 +01:00
Wendelin
5c3cf17df9 Merge branch 'dev' of github.com:home-assistant/frontend into add-automation-element-by-target 2025-11-10 17:15:28 +01:00
Wendelin
e905fa6f23 Add tree 2025-11-10 17:12:13 +01:00
61 changed files with 6236 additions and 1630 deletions

View File

@@ -52,7 +52,7 @@
"@fullcalendar/list": "6.1.19",
"@fullcalendar/luxon3": "6.1.19",
"@fullcalendar/timegrid": "6.1.19",
"@home-assistant/webawesome": "3.0.0",
"@home-assistant/webawesome": "3.0.0-ha.0",
"@lezer/highlight": "1.2.3",
"@lit-labs/motion": "1.0.9",
"@lit-labs/observers": "2.0.6",

View 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;
}
}

View File

@@ -6,7 +6,7 @@ import {
mdiHomeFloor3,
mdiHomeFloorNegative1,
} from "@mdi/js";
import { LitElement, html } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { FloorRegistryEntry } from "../data/floor_registry";
import "./ha-icon";
@@ -48,7 +48,7 @@ export const floorDefaultIcon = (floor: Pick<FloorRegistryEntry, "level">) => {
@customElement("ha-floor-icon")
export class HaFloorIcon extends LitElement {
@property({ attribute: false }) public floor!: Pick<
@property({ attribute: false }) public floor?: Pick<
FloorRegistryEntry,
"icon" | "level"
>;
@@ -56,6 +56,9 @@ export class HaFloorIcon extends LitElement {
@property() public icon?: string;
protected render() {
if (!this.floor) {
return nothing;
}
if (this.floor.icon) {
return html`<ha-icon .icon=${this.floor.icon}></ha-icon>`;
}

View File

@@ -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);

View File

@@ -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

View File

@@ -154,7 +154,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
}
return this._getLabelsMemoized(
this.hass,
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._labels,
this.includeDomains,
this.excludeDomains,

View File

@@ -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);
}
`,
];
}

View File

@@ -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")

View File

@@ -192,7 +192,7 @@ export class HaPickerComboBox extends LitElement {
@focus=${this._focusList}
@visibilityChanged=${this._visibilityChanged}
>
</lit-virtualizer> `;
</lit-virtualizer>`;
}
private _renderSectionButtons() {

View File

@@ -0,0 +1,28 @@
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-section-title")
class HaSectionTitle extends LitElement {
protected render() {
return html`<slot></slot>`;
}
static styles = css`
:host {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
box-sizing: border-box;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-section-title": HaSectionTitle;
}
}

View File

@@ -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) {

View File

@@ -30,6 +30,7 @@ import {
areaMeetsFilter,
deviceMeetsFilter,
entityRegMeetsFilter,
getTargetComboBoxItemType,
type TargetType,
type TargetTypeFloorless,
} from "../data/target";
@@ -47,7 +48,6 @@ import "./ha-tree-indicator";
import "./target-picker/ha-target-picker-item-group";
import "./target-picker/ha-target-picker-value-chip";
const EMPTY_SEARCH = "___EMPTY_SEARCH___";
const SEPARATOR = "________";
const CREATE_ID = "___create-new-entity___";
@@ -634,35 +634,6 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
private _getRowType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === EMPTY_SEARCH) {
return "empty";
}
return "label";
};
private _sectionTitleFunction = ({
firstIndex,
lastIndex,
@@ -686,7 +657,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return undefined;
}
const type = this._getRowType(firstItem as PickerComboBoxItem);
const type = getTargetComboBoxItemType(firstItem as PickerComboBoxItem);
const translationType:
| "areas"
| "entities"
@@ -858,7 +829,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (!filterType || filterType === "label") {
let labels = this._getLabelsMemoized(
this.hass,
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._labelRegistry,
includeDomains,
undefined,
@@ -974,7 +948,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return nothing;
}
const type = this._getRowType(item);
const type = getTargetComboBoxItemType(item);
let hasFloor = false;
let rtl = false;
let showEntityId = false;

View File

@@ -235,7 +235,7 @@ export class HaWaDialog extends LitElement {
}
:host([width="large"]) wa-dialog {
--width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
--width: min(var(--ha-dialog-width-lg, 1024px), var(--full-width));
}
:host([width="full"]) wa-dialog {

View File

@@ -21,11 +21,52 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
area?: AreaRegistryEntry;
}
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
floor?: FloorRegistryEntry;
areas: FloorComboBoxItem[];
}
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
areas: FloorComboBoxItem[];
}
export interface AreaFloorValue {
id: string;
type: "floor" | "area";
}
export const getAreasNestedInFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors,
true
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
export const getAreasAndFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
@@ -40,7 +81,43 @@ export const getAreasAndFloors = (
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
): FloorComboBoxItem[] => {
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors
) as FloorComboBoxItem[];
const getAreasAndFloorsItems = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[],
nested = false
): (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] => {
const floors = Object.values(haFloors);
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
@@ -181,7 +258,11 @@ export const getAreasAndFloors = (
const hierarchy = getAreasFloorHierarchy(floors, outputAreas);
const items: FloorComboBoxItem[] = [];
const items: (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] = [];
hierarchy.floors.forEach((f) => {
const floor = haFloors[f.id];
@@ -196,7 +277,7 @@ export const getAreasAndFloors = (
})
.flat();
items.push({
const floorItem: FloorComboBoxItem | FloorNestedComboBoxItem = {
id: formatId({ id: floor.floor_id, type: "floor" }),
type: "floor",
primary: floorName,
@@ -208,41 +289,53 @@ export const getAreasAndFloors = (
...floor.aliases,
...areaSearchLabels,
],
});
};
items.push(
...floorAreas.map((area) => {
const areaName = computeAreaName(area);
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName || area.area_id,
area: area,
icon: area.icon || undefined,
search_labels: [
area.area_id,
...(areaName ? [areaName] : []),
...area.aliases,
],
};
})
);
});
items.push(floorItem);
items.push(
...hierarchy.areas.map((areaId) => {
const area = haAreas[areaId];
const areaName = computeAreaName(area) || area.area_id;
const floorAreasItems = floorAreas.map((area) => {
const areaName = computeAreaName(area);
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
primary: areaName || area.area_id,
area: area,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
search_labels: [
area.area_id,
...(areaName ? [areaName] : []),
...area.aliases,
],
};
})
);
});
if (nested && floor) {
(floorItem as unknown as FloorNestedComboBoxItem).areas = floorAreasItems;
} else {
items.push(...floorAreasItems);
}
});
const unassignedAreaItems = hierarchy.areas.map((areaId) => {
const area = haAreas[areaId];
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
});
if (nested && unassignedAreaItems.length) {
items.push({
areas: unassignedAreaItems,
} as UnassignedAreasFloorComboBoxItem);
} else {
items.push(...unassignedAreaItems);
}
return items;
};

View File

@@ -1,7 +1,10 @@
import { stringCompare } from "../common/string/compare";
import type { HomeAssistant } from "../types";
import type { DeviceRegistryEntry } from "./device_registry";
import type { EntityRegistryEntry } from "./entity_registry";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import type { RegistryEntry } from "./registry";
export { subscribeAreaRegistry } from "./ws-area_registry";
@@ -18,7 +21,10 @@ export interface AreaRegistryEntry extends RegistryEntry {
temperature_entity_id: string | null;
}
export type AreaEntityLookup = Record<string, EntityRegistryEntry[]>;
export type AreaEntityLookup = Record<
string,
(EntityRegistryEntry | EntityRegistryDisplayEntry)[]
>;
export type AreaDeviceLookup = Record<string, DeviceRegistryEntry[]>;
@@ -69,11 +75,17 @@ export const reorderAreaRegistryEntries = (
});
export const getAreaEntityLookup = (
entities: EntityRegistryEntry[]
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
filterHidden = false
): AreaEntityLookup => {
const areaEntityLookup: AreaEntityLookup = {};
for (const entity of entities) {
if (!entity.area_id) {
if (
!entity.area_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
continue;
}
if (!(entity.area_id in areaEntityLookup)) {

View File

@@ -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;
}

View File

@@ -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;
};

View File

@@ -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) : "_";

View File

@@ -50,7 +50,11 @@ export type DeviceEntityDisplayLookup = Record<
EntityRegistryDisplayEntry[]
>;
export type DeviceEntityLookup = Record<string, EntityRegistryEntry[]>;
export type DeviceEntityLookup<
T extends EntityRegistryEntry | EntityRegistryDisplayEntry =
| EntityRegistryEntry
| EntityRegistryDisplayEntry,
> = Record<string, T[]>;
export interface DeviceRegistryEntryMutableParams {
area_id?: string | null;
@@ -107,11 +111,17 @@ export const sortDeviceRegistryByName = (
);
export const getDeviceEntityLookup = (
entities: EntityRegistryEntry[]
entities: (EntityRegistryEntry | EntityRegistryDisplayEntry)[],
filterHidden = false
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
if (
!entity.device_id ||
(filterHidden &&
((entity as EntityRegistryDisplayEntry).hidden ||
(entity as EntityRegistryEntry).hidden_by))
) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {

View File

@@ -1,14 +0,0 @@
import type { HomeAssistant } from "../types";
export interface ESPHomeEncryptionKey {
encryption_key: string;
}
export const fetchESPHomeEncryptionKey = (
hass: HomeAssistant,
entry_id: string
): Promise<ESPHomeEncryptionKey> =>
hass.callWS({
type: "esphome/get_encryption_key",
entry_id,
});

View File

@@ -7,8 +7,8 @@ export interface CoreFrontendUserData {
}
export interface SidebarFrontendUserData {
panelOrder: string[];
hiddenPanels: string[];
panelOrder?: string[];
hiddenPanels?: string[];
}
export interface CoreFrontendSystemData {

View File

@@ -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

View File

@@ -1,8 +1,8 @@
import type { Connection } from "home-assistant-js-websocket";
import { createCollection } from "home-assistant-js-websocket";
import type { LocalizeFunc } from "../common/translations/localize";
import type { HomeAssistant } from "../types";
import { debounce } from "../common/util/debounce";
import type { HomeAssistant } from "../types";
export const integrationsWithPanel = {
bluetooth: "config/bluetooth",
@@ -25,6 +25,8 @@ export type IntegrationType =
| "entity"
| "system";
export type DomainManifestLookup = Record<string, IntegrationManifest>;
export interface IntegrationManifest {
is_built_in: boolean;
overwrites_built_in?: boolean;

View File

@@ -101,7 +101,10 @@ export const deleteLabelRegistryEntry = (
});
export const getLabels = (
hass: HomeAssistant,
hassStates: HomeAssistant["states"],
hassAreas: HomeAssistant["areas"],
hassDevices: HomeAssistant["devices"],
hassEntities: HomeAssistant["entities"],
labels?: LabelRegistryEntry[],
includeDomains?: string[],
excludeDomains?: string[],
@@ -115,8 +118,8 @@ export const getLabels = (
return [];
}
const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities);
const devices = Object.values(hassDevices);
const entities = Object.values(hassEntities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
@@ -170,7 +173,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hass.states[entity.entity_id];
const stateObj = hassStates[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -181,8 +184,9 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hass.states[entity.entity_id];
const stateObj = hassStates[entity.entity_id];
return (
stateObj &&
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
@@ -200,7 +204,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hass.states[entity.entity_id];
const stateObj = hassStates[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -208,7 +212,7 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hass.states[entity.entity_id];
const stateObj = hassStates[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -245,8 +249,8 @@ export const getLabels = (
if (areaIds) {
areaIds.forEach((areaId) => {
const area = hass.areas[areaId];
area.labels.forEach((label) => usedLabels.add(label));
const area = hassAreas[areaId];
area?.labels.forEach((label) => usedLabels.add(label));
});
}

View File

@@ -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"];

View File

@@ -1,15 +1,30 @@
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
import type { HomeAssistant } from "../types";
import type { FloorComboBoxItem } from "./area_floor";
import type { AreaRegistryEntry } from "./area_registry";
import type { DeviceRegistryEntry } from "./device_registry";
import type { DevicePickerItem, DeviceRegistryEntry } from "./device_registry";
import type { HaEntityPickerEntityFilterFunc } from "./entity";
import type { EntityRegistryDisplayEntry } from "./entity_registry";
import type {
EntityComboBoxItem,
EntityRegistryDisplayEntry,
} from "./entity_registry";
export const TARGET_SEPARATOR = "________";
export type TargetType = "entity" | "device" | "area" | "label" | "floor";
export type TargetTypeFloorless = Exclude<TargetType, "floor">;
export interface SingleHassServiceTarget {
entity_id?: string;
device_id?: string;
area_id?: string;
floor_id?: string;
label_id?: string;
}
export interface ExtractFromTargetResult {
missing_areas: string[];
missing_devices: string[];
@@ -35,6 +50,28 @@ export const extractFromTarget = async (
target,
});
export const getTriggersForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_triggers_for_target",
target,
expand_group: expandGroup,
});
export const getServicesForTarget = async (
callWS: HomeAssistant["callWS"],
target: HassServiceTarget,
expandGroup = true
) =>
callWS<string[]>({
type: "get_services_for_target",
target,
expand_group: expandGroup,
});
export const areaMeetsFilter = (
area: AreaRegistryEntry,
devices: HomeAssistant["devices"],
@@ -162,3 +199,32 @@ export const entityRegMeetsFilter = (
}
return true;
};
export const getTargetComboBoxItemType = (
item:
| PickerComboBoxItem
| (FloorComboBoxItem & { last?: boolean | undefined })
| EntityComboBoxItem
| DevicePickerItem
) => {
if (
(item as FloorComboBoxItem).type === "area" ||
(item as FloorComboBoxItem).type === "floor"
) {
return (item as FloorComboBoxItem).type;
}
if ("domain" in item) {
return "device";
}
if ("stateObj" in item) {
return "entity";
}
if (item.id === "___EMPTY_SEARCH___") {
return "empty";
}
return "label";
};

View File

@@ -75,7 +75,8 @@ export type TranslationCategory =
| "preview_features"
| "selector"
| "services"
| "triggers";
| "triggers"
| "conditions";
export const subscribeTranslationPreferences = (
hass: HomeAssistant,

View File

@@ -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, {

View 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;
};

View File

@@ -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 & {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,384 @@
import {
mdiInformationOutline,
mdiLabel,
mdiPlus,
mdiTextureBox,
} from "@mdi/js";
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
import {
customElement,
eventOptions,
property,
query,
state,
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-domain-icon";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import type { ConfigEntry } from "../../../../data/config_entries";
import type { HomeAssistant } from "../../../../types";
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
type Target = [string, string | undefined, string | undefined];
@customElement("ha-automation-add-items")
export class HaAutomationAddItems extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public items?: {
title: string;
items: AddAutomationElementListItem[];
}[];
@property() public error?: string;
@property({ attribute: "select-label" }) public selectLabel!: string;
@property({ attribute: "empty-label" }) public emptyLabel!: string;
@property({ attribute: false }) public target?: Target;
@property({ attribute: false }) public getLabel!: (
id: string
) => { name: string; icon?: string } | undefined;
@property({ attribute: false }) public configEntryLookup: Record<
string,
ConfigEntry
> = {};
@property({ type: Boolean, attribute: "tooltip-description" })
public tooltipDescription = false;
@state() private _itemsScrolled = false;
@query(".items")
private _itemsDiv!: HTMLDivElement;
protected render() {
return html`<div
class=${classMap({
items: true,
blank: this.error || !this.items || !this.items.length,
error: this.error,
scrolled: this._itemsScrolled,
})}
@scroll=${this._onItemsScroll}
>
${!this.items && !this.error
? this.selectLabel
: this.error
? html`${this.error}
<div>${this._renderTarget(this.target)}</div>`
: this.items && !this.items.length
? html`${this.emptyLabel}
${this.target
? html`<div>${this._renderTarget(this.target)}</div>`
: nothing}`
: repeat(
this.items,
(_, index) => `item-group-${index}`,
(itemGroup) =>
this._renderItemList(itemGroup.title, itemGroup.items)
)}
</div>`;
}
private _renderItemList(title, items?: AddAutomationElementListItem[]) {
if (!items || !items.length) {
return nothing;
}
return html`
<div class="items-title">${title}</div>
<ha-md-list>
${repeat(
items,
(item) => item.key,
(item) => html`
<ha-md-list-item
interactive
type="button"
.value=${item.key}
@click=${this._selected}
>
<div slot="headline" class=${this.target ? "item-headline" : ""}>
${item.name}${this._renderTarget(this.target)}
</div>
${!this.tooltipDescription && item.description
? html`<div slot="supporting-text">${item.description}</div>`
: nothing}
${item.icon
? html`<span slot="start">${item.icon}</span>`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: nothing}
${this.tooltipDescription && item.description
? html`<ha-svg-icon
tabindex="0"
id=${`description-tooltip-${item.key}`}
slot="end"
.path=${mdiInformationOutline}
@click=${stopPropagation}
></ha-svg-icon>
<ha-tooltip
.for=${`description-tooltip-${item.key}`}
@wa-show=${stopPropagation}
@wa-hide=${stopPropagation}
@wa-after-hide=${stopPropagation}
@wa-after-show=${stopPropagation}
>${item.description}</ha-tooltip
> `
: nothing}
<ha-svg-icon
slot="end"
class="plus"
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
`
)}
</ha-md-list>
`;
}
private _renderTarget = memoizeOne((target?: Target) => {
if (!target) {
return nothing;
}
return html`<div class="selected-target">
${this._getSelectedTargetIcon(target[0], target[1])}
<div class="label">${target[2]}</div>
</div>`;
});
private _getSelectedTargetIcon(
targetType: string,
targetId: string | undefined
): TemplateResult | typeof nothing {
if (!targetId) {
return nothing;
}
if (targetType === "floor") {
return html`<ha-floor-icon
.floor=${this.hass.floors[targetId]}
></ha-floor-icon>`;
}
if (targetType === "area" && this.hass.areas[targetId]) {
const area = this.hass.areas[targetId];
if (area.icon) {
return html`<ha-icon .icon=${area.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiTextureBox}></ha-svg-icon>`;
}
if (targetType === "device" && this.hass.devices[targetId]) {
const device = this.hass.devices[targetId];
const configEntry = device.primary_config_entry
? this.configEntryLookup[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
if (domain) {
return html`<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>`;
}
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
if (stateObj) {
return html`<state-badge
.stateObj=${stateObj}
.hass=${this.hass}
.stateColor=${false}
></state-badge>`;
}
}
if (targetType === "label") {
const label = this.getLabel(targetId);
if (label?.icon) {
return html`<ha-icon .icon=${label.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiLabel}></ha-svg-icon>`;
}
return nothing;
}
private _selected(ev) {
const item = ev.currentTarget;
fireEvent(this, "value-changed", {
value: item.value,
});
}
@eventOptions({ passive: true })
private _onItemsScroll(ev) {
const top = ev.target.scrollTop ?? 0;
this._itemsScrolled = top > 0;
}
public override scrollTo(options?: ScrollToOptions): void;
public override scrollTo(x: number, y: number): void;
public override scrollTo(
xOrOptions?: number | ScrollToOptions,
y?: number
): void {
if (typeof xOrOptions === "number") {
this._itemsDiv?.scrollTo(xOrOptions, y!);
} else {
this._itemsDiv?.scrollTo(xOrOptions);
}
}
static styles = css`
:host {
display: flex;
}
.items {
display: flex;
flex-direction: column;
overflow: auto;
flex: 1;
}
.items.blank {
border-radius: var(--ha-border-radius-xl);
background-color: var(--ha-color-surface-default);
align-items: center;
color: var(--ha-color-text-secondary);
padding: var(--ha-space-0);
margin: var(--ha-space-3) var(--ha-space-4)
max(var(--safe-area-inset-bottom), var(--ha-space-3));
line-height: var(--ha-line-height-expanded);
justify-content: center;
}
.items.error {
background-color: var(--ha-color-fill-danger-quiet-resting);
color: var(--ha-color-on-danger-normal);
}
.items ha-md-list {
--md-list-item-two-line-container-height: var(--ha-space-12);
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-2);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-family-body);
--ha-md-list-item-gap: var(--ha-space-3);
gap: var(--ha-space-2);
padding: var(--ha-space-0) var(--ha-space-4);
}
.items ha-md-list ha-md-list-item {
border-radius: var(--ha-border-radius-lg);
border: 1px solid var(--ha-color-border-neutral-quiet);
}
.items ha-md-list {
padding-bottom: max(var(--safe-area-inset-bottom), var(--ha-space-3));
}
.items .item-headline {
display: flex;
align-items: center;
gap: var(--ha-space-1);
min-height: var(--ha-space-9);
flex-wrap: wrap;
}
.items-title {
position: sticky;
display: flex;
align-items: center;
font-weight: var(--ha-font-weight-medium);
padding-top: var(--ha-space-2);
padding-bottom: var(--ha-space-2);
padding-inline-start: var(--ha-space-8);
padding-inline-end: var(--ha-space-3);
top: 0;
z-index: 1;
background-color: var(--card-background-color);
}
ha-bottom-sheet .items-title {
padding-top: var(--ha-space-3);
}
.scrolled .items-title:first-of-type {
box-shadow: var(--bar-box-shadow);
border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
}
ha-icon-next {
width: var(--ha-space-6);
}
ha-svg-icon.plus {
color: var(--primary-color);
}
.selected-target {
display: inline-flex;
gap: var(--ha-space-1);
justify-content: center;
align-items: center;
border-radius: var(--ha-border-radius-md);
background: var(--ha-color-fill-neutral-normal-resting);
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
color: var(--ha-color-on-neutral-normal);
overflow: hidden;
}
.selected-target .label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selected-target ha-icon,
.selected-target ha-svg-icon,
.selected-target state-badge,
.selected-target ha-domain-icon {
display: flex;
padding: var(--ha-space-1) 0;
}
.selected-target state-badge {
--mdc-icon-size: 20px;
}
.selected-target state-badge,
.selected-target ha-domain-icon {
width: 24px;
height: 24px;
filter: grayscale(100%);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-add-items": HaAutomationAddItems;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View File

@@ -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

View File

@@ -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(

View File

@@ -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;
}
}

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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}

View File

@@ -1,52 +0,0 @@
import { mdiKey } from "@mdi/js";
import { getConfigEntries } from "../../../../../../data/config_entries";
import type { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchESPHomeEncryptionKey } from "../../../../../../data/esphome";
import type { HomeAssistant } from "../../../../../../types";
import { showESPHomeEncryptionKeyDialog } from "../../../../integrations/integration-panels/esphome/show-dialog-esphome-encryption-key";
import type { DeviceAction } from "../../../ha-config-device-page";
export const getESPHomeDeviceActions = async (
el: HTMLElement,
hass: HomeAssistant,
device: DeviceRegistryEntry
): Promise<DeviceAction[]> => {
const actions: DeviceAction[] = [];
const configEntries = await getConfigEntries(hass, {
domain: "esphome",
});
const configEntry = configEntries.find((entry) =>
device.config_entries.includes(entry.entry_id)
);
if (!configEntry) {
return [];
}
const entryId = configEntry.entry_id;
try {
const encryptionKey = await fetchESPHomeEncryptionKey(hass, entryId);
if (encryptionKey.encryption_key) {
actions.push({
label: hass.localize(
"ui.panel.config.devices.esphome.show_encryption_key"
),
icon: mdiKey,
action: () =>
showESPHomeEncryptionKeyDialog(el, {
entry_id: entryId,
encryption_key: encryptionKey.encryption_key,
}),
});
}
} catch (err) {
// eslint-disable-next-line no-console
console.error("Failed to fetch ESPHome encryption key:", err);
}
return actions;
};

View File

@@ -1162,17 +1162,6 @@ export class HaConfigDevicePage extends LitElement {
);
deviceActions.push(...actions);
}
if (domains.includes("esphome")) {
const esphome = await import(
"./device-detail/integration-elements/esphome/device-actions"
);
const actions = await esphome.getESPHomeDeviceActions(
this,
this.hass,
device
);
deviceActions.push(...actions);
}
if (domains.includes("matter")) {
const matter = await import(
"./device-detail/integration-elements/matter/device-actions"

View File

@@ -1,19 +1,19 @@
import { consume } from "@lit/context";
import {
mdiCancel,
mdiChevronRight,
mdiDelete,
mdiDotsVertical,
mdiMenuDown,
mdiPlus,
mdiTextureBox,
mdiCancel,
mdiDelete,
} from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { ResizeController } from "@lit-labs/observers/resize-controller";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { customElement, property, state, query } from "lit/decorators";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeCssColor } from "../../../common/color/compute-color";
import { formatShortDateTime } from "../../../common/datetime/format_date_time";
@@ -68,8 +68,8 @@ import type {
DeviceRegistryEntry,
} from "../../../data/device_registry";
import {
updateDeviceRegistryEntry,
removeConfigEntryFromDevice,
updateDeviceRegistryEntry,
} from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import {
@@ -86,8 +86,8 @@ import {
showAlertDialog,
showConfirmationDialog,
} from "../../../dialogs/generic/show-dialog-box";
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import "../../../layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant, Route } from "../../../types";
@@ -318,7 +318,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) {
})
);
const deviceEntityLookup: DeviceEntityLookup = {};
const deviceEntityLookup: DeviceEntityLookup<EntityRegistryEntry> = {};
for (const entity of entities) {
if (!entity.device_id) {
continue;

View File

@@ -1,137 +0,0 @@
import { mdiClose, mdiContentCopy } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { copyToClipboard } from "../../../../../common/util/copy-clipboard";
import "../../../../../components/ha-dialog-header";
import "../../../../../components/ha-icon-button";
import "../../../../../components/ha-wa-dialog";
import { haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import { showToast } from "../../../../../util/toast";
import type { ESPHomeEncryptionKeyDialogParams } from "./show-dialog-esphome-encryption-key";
@customElement("dialog-esphome-encryption-key")
class DialogESPHomeEncryptionKey extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _params?: ESPHomeEncryptionKeyDialogParams;
public async showDialog(
params: ESPHomeEncryptionKeyDialogParams
): Promise<void> {
this._params = params;
}
public closeDialog(): void {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render() {
if (!this._params) {
return nothing;
}
return html`
<ha-wa-dialog
open
@closed=${this.closeDialog}
hideActions
header-title=${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_title"
)}
>
<ha-dialog-header slot="heading">
<ha-icon-button
slot="navigationIcon"
dialogAction="cancel"
.label=${this.hass.localize("ui.common.close")}
.path=${mdiClose}
></ha-icon-button>
<span slot="title">
${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_title"
)}
</span>
</ha-dialog-header>
<div class="content">
<p>
${this.hass.localize(
"ui.panel.config.devices.esphome.encryption_key_description"
)}
</p>
<div class="key-row">
<div class="key-container">
<code>${this._params.encryption_key}</code>
</div>
<ha-icon-button
@click=${this._copyToClipboard}
.label=${this.hass.localize("ui.common.copy")}
.path=${mdiContentCopy}
></ha-icon-button>
</div>
</div>
</ha-wa-dialog>
`;
}
private async _copyToClipboard(): Promise<void> {
if (!this._params?.encryption_key) {
return;
}
await copyToClipboard(this._params.encryption_key);
showToast(this, {
message: this.hass.localize("ui.common.copied_clipboard"),
});
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
ha-dialog {
--mdc-dialog-max-width: 500px;
}
.content {
display: flex;
flex-direction: column;
gap: var(--ha-space-6);
}
.key-row {
display: flex;
gap: var(--ha-space-2);
align-items: center;
}
.key-container {
flex: 1;
border-radius: var(--ha-space-2);
border: 1px solid var(--divider-color);
background-color: var(
--code-editor-background-color,
var(--secondary-background-color)
);
padding: var(--ha-space-3);
overflow: auto;
}
p {
margin: 0;
color: var(--secondary-text-color);
line-height: var(--ha-line-height-condensed);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-esphome-encryption-key": DialogESPHomeEncryptionKey;
}
}

View File

@@ -1,20 +0,0 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
export interface ESPHomeEncryptionKeyDialogParams {
entry_id: string;
encryption_key: string;
}
export const loadESPHomeEncryptionKeyDialog = () =>
import("./dialog-esphome-encryption-key");
export const showESPHomeEncryptionKeyDialog = (
element: HTMLElement,
dialogParams: ESPHomeEncryptionKeyDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-esphome-encryption-key",
dialogImport: loadESPHomeEncryptionKeyDialog,
dialogParams,
});
};

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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",

View File

@@ -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,

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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

View File

@@ -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,

View File

@@ -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 "";

View File

@@ -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);
}
`;

View File

@@ -52,6 +52,8 @@ export const waColorStyles = css`
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
--wa-color-text-quiet: var(--ha-color-text-secondary);
--wa-color-text-normal: var(--ha-color-text-primary);
--wa-color-surface-default: var(--card-background-color);
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
@@ -62,5 +64,7 @@ export const waColorStyles = css`
--wa-focus-ring-color: var(--ha-color-neutral-60);
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
--wa-color-text-normal: var(--ha-color-text-primary);
}
`;

View File

@@ -9,12 +9,16 @@ export const waMainStyles = css`
--wa-focus-ring-offset: 2px;
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
--wa-space-xs: var(--ha-space-2);
--wa-space-m: var(--ha-space-4);
--wa-space-l: var(--ha-space-6);
--wa-space-xl: var(--ha-space-8);
--wa-form-control-padding-block: 0.75em;
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-transition-normal: 150ms;
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
@@ -28,6 +32,7 @@ export const waMainStyles = css`
--wa-line-height-condensed: var(--ha-line-height-condensed);
--wa-font-size-m: var(--ha-font-size-m);
--wa-shadow-s: var(--ha-box-shadow-s);
--wa-shadow-m: var(--ha-box-shadow-m);
--wa-shadow-l: var(--ha-box-shadow-l);

View File

@@ -2217,7 +2217,9 @@
"sidebar_toggle": "Sidebar toggle",
"edit_sidebar": "Edit sidebar",
"edit_subtitle": "Synced on all devices",
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device."
"migrate_to_user_data": "This will change the sidebar on all the devices you are logged in to. To create a sidebar per device, you should use a different user for that device.",
"reset_to_defaults": "Reset to defaults",
"reset_confirmation": "Are you sure you want to reset the sidebar to its default configuration? This will restore the original order and visibility of all panels."
},
"panel": {
"home": {
@@ -3508,6 +3510,7 @@
"edit": "Edit",
"delete": "Delete",
"add_dashboard": "Add dashboard",
"set_as_default": "Set as default",
"type": {
"user_created": "User created",
"built_in": "Built-in"
@@ -3516,7 +3519,7 @@
"confirm_delete_title": "Delete {dashboard_title}?",
"confirm_delete_text": "This dashboard will be permanently deleted.",
"cant_edit_yaml": "Dashboards created in YAML cannot be edited from the UI. Change them in configuration.yaml.",
"cant_edit_default": "The default dashboard, Overview, cannot be edited from the UI. You can hide it by setting another dashboard as default.",
"cant_edit_lovelace": "The Overview dashboard title and icon cannot be changed. You can create a new dashboard to get more customization options.",
"detail": {
"edit_dashboard": "Edit dashboard",
"new_dashboard": "Add new dashboard",
@@ -3533,9 +3536,7 @@
"set_default": "Set as default",
"remove_default": "Remove as default",
"set_default_confirm_title": "Set as default dashboard?",
"set_default_confirm_text": "This will replace the current default dashboard. Users can still override their default dashboard in their profile settings.",
"remove_default_confirm_title": "Remove default dashboard?",
"remove_default_confirm_text": "The default dashboard will be changed to Overview for every user. Users can still override their default dashboard in their profile settings."
"set_default_confirm_text": "This dashboard will be shown to all users when opening Home Assistant. Each user can change this in their profile."
}
},
"resources": {
@@ -4009,7 +4010,25 @@
"item_pasted": "{item} pasted",
"ctrl": "Ctrl",
"del": "Del",
"targets": "Targets",
"select_target": "Select a target",
"home": "Home",
"unassigned": "Unassigned",
"blocks": "Blocks",
"show_more": "Show more",
"unassigned_entities": "Unassigned entities",
"unassigned_devices": "Unassigned devices",
"empty_section_search": {
"block": "No blocks found for {term}",
"entity": "No entities found for {term}",
"device": "No devices found for {term}",
"area": "No areas or floors found for {term}",
"label": "No labels found for {term}"
},
"load_target_items_failed": "Failed to load target items for",
"other_areas": "Other areas",
"services": "Services",
"helpers": "Helpers",
"triggers": {
"name": "Triggers",
"header": "When",
@@ -4017,7 +4036,10 @@
"learn_more": "Learn more about triggers",
"triggered": "Triggered",
"add": "Add trigger",
"empty_search": "No triggers found for {term}",
"empty_search": {
"global": "No triggers and targets found for {term}",
"item": "No triggers found for {term}"
},
"id": "Trigger ID",
"optional": "Optional",
"edit_id": "Edit ID",
@@ -4038,6 +4060,7 @@
"copied_to_clipboard": "Trigger copied to clipboard",
"cut_to_clipboard": "Trigger cut to clipboard",
"select": "Select a trigger",
"no_items_for_target": "No triggers available for",
"groups": {
"device": {
"label": "Device"
@@ -4279,7 +4302,10 @@
"description": "All conditions added here need to be satisfied for the automation to run. A condition can be satisfied or not at any given time, for example: ''If {user} is home''. You can use building blocks to create more complex conditions.",
"learn_more": "Learn more about conditions",
"add": "Add condition",
"empty_search": "No conditions and blocks found for {term}",
"empty_search": {
"global": "No conditions, blocks and targets found for {term}",
"item": "No conditions found for {term}"
},
"add_building_block": "Add building block",
"test": "Test",
"testing_error": "Condition did not pass",
@@ -4302,6 +4328,7 @@
"copied_to_clipboard": "Condition copied to clipboard",
"cut_to_clipboard": "Condition cut to clipboard",
"select": "Select a condition",
"no_items_for_target": "No conditions available for",
"groups": {
"device": {
"label": "Device"
@@ -4447,7 +4474,10 @@
"description": "All actions added here will be performed in sequence when the automation runs. An action usually controls one of your areas, devices, or entities, for example: 'Turn on the lights'. You can use building blocks to create more complex sequences of actions.",
"learn_more": "Learn more about actions",
"add": "Add action",
"empty_search": "No actions and blocks found for {term}",
"empty_search": {
"global": "No actions, blocks and targets found for {term}",
"item": "No actions found for {term}"
},
"add_building_block": "Add building block",
"invalid_action": "Invalid action",
"run": "Run action",
@@ -4472,6 +4502,7 @@
"copied_to_clipboard": "Action copied to clipboard",
"cut_to_clipboard": "Action cut to clipboard",
"select": "Select an action",
"no_items_for_target": "No actions available for",
"groups": {
"device_id": {
"label": "Device"
@@ -5412,11 +5443,6 @@
"partial_failure": "Some devices failed to delete successfully. Check system logs for more information."
}
}
},
"esphome": {
"show_encryption_key": "Show encryption key",
"encryption_key_title": "ESPHome Encryption Key",
"encryption_key_description": "This is the encryption key for your ESPHome device. Keep it in a safe place, as you may need it when transferring devices between Home Assistant instances."
}
},
"entities": {
@@ -7163,6 +7189,7 @@
"energy_usage_graph": {
"total_consumed": "Total consumed {num} kWh",
"total_returned": "Total returned {num} kWh",
"total_usage": "{num} kWh used",
"combined_from_grid": "Combined from grid",
"consumed_solar": "Consumed solar",
"consumed_battery": "Consumed battery"
@@ -7307,6 +7334,7 @@
"editor": {
"header": "Edit UI",
"yaml_unsupported": "The edit UI is not available when in YAML mode.",
"undo_redo_failed_to_apply_changes": "Unable to apply changes: {error}",
"menu": {
"open": "Open dashboard menu",
"raw_editor": "Raw configuration editor",

View File

@@ -1940,9 +1940,9 @@ __metadata:
languageName: node
linkType: hard
"@home-assistant/webawesome@npm:3.0.0":
version: 3.0.0
resolution: "@home-assistant/webawesome@npm:3.0.0"
"@home-assistant/webawesome@npm:3.0.0-ha.0":
version: 3.0.0-ha.0
resolution: "@home-assistant/webawesome@npm:3.0.0-ha.0"
dependencies:
"@ctrl/tinycolor": "npm:4.1.0"
"@floating-ui/dom": "npm:^1.6.13"
@@ -1953,7 +1953,7 @@ __metadata:
lit: "npm:^3.2.1"
nanoid: "npm:^5.1.5"
qr-creator: "npm:^1.0.0"
checksum: 10/03400894cfee8548fd5b1f5c56d31d253830e704b18ba69d36ce6b761d8b1bef2fb52cffba8d9b033033bb582f2f51a2d6444d82622f66d70150e2104fcb49e2
checksum: 10/2034d498d5b26bb0573ebc2c9aadd144604bb48c04becbae0c67b16857d8e5d6562626e795974362c3fc41e9b593a9005595d8b5ff434b1569b2d724af13043b
languageName: node
linkType: hard
@@ -9226,7 +9226,7 @@ __metadata:
"@fullcalendar/list": "npm:6.1.19"
"@fullcalendar/luxon3": "npm:6.1.19"
"@fullcalendar/timegrid": "npm:6.1.19"
"@home-assistant/webawesome": "npm:3.0.0"
"@home-assistant/webawesome": "npm:3.0.0-ha.0"
"@lezer/highlight": "npm:1.2.3"
"@lit-labs/motion": "npm:1.0.9"
"@lit-labs/observers": "npm:2.0.6"