Compare commits

...

104 Commits

Author SHA1 Message Date
Bram Kragten 68f75c82eb Bumped version to 20260527.3 2026-06-02 23:55:02 +02:00
Bram Kragten 6660e4799c Add tags in app store too, plus show if addon is installed already (#52373) 2026-06-02 23:54:24 +02:00
Petar Petrov 08bfafea21 Fix raw div tag showing in Sankey chart tooltips (#52365)
Fix raw div tag showing in sankey chart tooltips
2026-06-02 23:54:23 +02:00
Bram Kragten 5677e60fcc Matter add device: change how main entity is found (#52361)
Don't search for a entity based on main entity but use entity_category
2026-06-02 23:54:22 +02:00
Bram Kragten 73557e6464 Migrate trigger behavior (#52360)
* Migrate trigger behavior

* Apply suggestions from code review

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>

---------

Co-authored-by: Simon Lamon <32477463+silamon@users.noreply.github.com>
2026-06-02 23:54:21 +02:00
Marcin Bauer e9e6c60d8b Move live-test indicator to badge on condition icon (#52352)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Wendelin <w@pe8.at>
2026-06-02 23:54:20 +02:00
Aidan Timson 1651c210be Improve messaging and consolidate add to dialogs (#52330) 2026-06-02 23:54:19 +02:00
Bram Kragten 927c036454 Bumped version to 20260527.2 2026-06-01 19:52:36 +02:00
Paul Bottein 0fefcf809f Fix vacuum and lawn mower features not showing default buttons (#52343) 2026-06-01 19:52:19 +02:00
Bram Kragten a176f3c1ef Allow to set refresh url while dialog is open, use for matter device (#52341)
Allow to set refresh dialog while dialog is open, use for matter device
2026-06-01 19:52:18 +02:00
Wendelin c5152c3472 App-Info: Hide app title on narrow (#52337)
Hide app title on narrow
2026-06-01 19:52:17 +02:00
Wendelin 0150337522 Fix picker default popover-placement (#52336) 2026-06-01 19:52:16 +02:00
Paul Bottein 5d55d543b1 Respect backend order for floors and areas in entity tree (#52329) 2026-06-01 19:52:14 +02:00
George Caliment 4805b22289 Fixed filter flex direction on mobile + removed unused classes (#52327)
* Fixed filter flex direction on mobile + removed unused classes

* Removed hard-coded height to fill all viewport
2026-06-01 19:52:13 +02:00
Simon Lamon 8de411abc3 Show all counter actions if none specified (#52317)
Show all actions if none specified
2026-06-01 19:52:12 +02:00
Jan-Philipp Benecke e455d4384a Use right token for topbar shadow transition (#52306) 2026-06-01 19:52:11 +02:00
karwosts b0dbd825c8 Fix behavior for move view left/right (#52300) 2026-06-01 19:52:10 +02:00
karwosts 69d0fcb666 Fix untracked legend in detail graph card (#52299) 2026-06-01 19:52:09 +02:00
Simon Lamon f7c3ed3b77 Ignore location in description (#52297) 2026-06-01 19:52:08 +02:00
Jan-Philipp Benecke 5ee5b5120e Add box-shadow transition to top app bar (#52292) 2026-06-01 19:52:07 +02:00
karwosts 58fc8160fd Fix missing location data in calendar (#52291) 2026-06-01 19:52:06 +02:00
Bram Kragten 30930e18ab Bumped version to 20260527.1 2026-05-28 16:47:56 +02:00
Paul Bottein 8d0978817d Don't lowercase translated default action label (#52283) 2026-05-28 16:45:20 +02:00
Paul Bottein fc684218ce Preserve PNG transparency on area pictures (#52282) 2026-05-28 16:45:18 +02:00
Paul Bottein 22f29b7561 Fix sun condition Between description showing reversed values (#52279) 2026-05-28 16:45:16 +02:00
Wendelin c7d48aba44 Fix automation add TCA paste (#52276)
Fix automation add paste
2026-05-28 16:45:15 +02:00
Wendelin aeb2285f30 App details improve mobile and icon (#52275)
* icon instead of logo, enable wrap

* Keep logo

* revert test url
2026-05-28 16:45:14 +02:00
Wendelin c692d7cd4e Card visibility-status use ha-alert (#52271) 2026-05-28 16:45:12 +02:00
Wendelin f2d7021a7d Fix automation note keyboard a11y (#52270) 2026-05-28 16:45:11 +02:00
Wendelin 3a649fba22 Fix automation behavior img file names (#52247)
fix behavior img names
2026-05-28 16:45:09 +02:00
Simon Lamon 5362b8f853 Don't redispatch the original event in a checklist item (#52242) 2026-05-28 16:45:08 +02:00
Wendelin d05800bda6 Fix ha-radio-option checked theming (#52237)
Update ha-radio-option theming to use checked-icon-color for text and border
2026-05-28 16:45:07 +02:00
Wendelin d67530ea37 Fix row target count flickering, keyboard nav, type device (#52236)
* Fix row target count flickering

* Add noninteractive for device, fix keyboard nav

* Noninteractive action, conditon

* Remove unsued hass

* invert noninteractive
2026-05-28 16:45:05 +02:00
Petar Petrov bbd7ef676e Render echarts tooltips with Lit templates (#52235)
* Render echarts tooltips with Lit templates

Replace raw HTML string interpolation in echarts tooltip formatters with Lit templates so user-controlled fields (entity friendly_name, device names, node labels) are auto-escaped instead of relying on per-string filterXSS. ha-chart-base now wraps any function tooltip.formatter into a stable per-formatter container and handles Lit TemplateResult / nothing / null returns; the public HaECOption type lets charts express Lit-returning formatters without per-callsite casts.

* Simplify

* Refactor _getSeries

* Small fix

* Fix merge mistake

* Marker component and wrapper test
2026-05-28 16:45:04 +02:00
Bram Kragten e39e1b3f5b Merge branch 'rc' into dev 2026-05-27 15:29:28 +02:00
Bram Kragten ff583d2274 Bumped version to 20260527.0 2026-05-27 15:26:39 +02:00
Wendelin d4de29e073 Rename automation trigger behavior options (#52224) 2026-05-27 15:24:53 +02:00
Wendelin 97dfed0cc4 Rename automation comments to note (#52219) 2026-05-27 15:23:27 +02:00
Bram Kragten 8b3df752da Add associated zone option for device trackers (#52211) 2026-05-27 15:18:01 +02:00
Paul Bottein 8c0d547962 Render small media browser thumbnails without blur (#52230)
* Render small media browser thumbnails without blur

* Only 16 pixels and no svg

* Skip brand url

* Media selector
2026-05-27 15:17:20 +02:00
Wendelin 5e3d84f0ad Add live test state message tooltip (#52233) 2026-05-27 15:08:43 +02:00
Petar Petrov b4e30bdf63 Fix energy compare bars stacking when compare month has more days (#52221) 2026-05-27 15:01:33 +02:00
Petar Petrov 4fcae4231c Remove redundant log-axis non-positive data preprocessing (#52222) 2026-05-27 14:59:37 +02:00
Wendelin 2aecf33955 Fix app details in tablet width (#52234) 2026-05-27 14:54:02 +02:00
Paulus Schoutsen 5f26a2b3da Show verify-email flash after cloud signup (#52232)
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-27 14:46:24 +02:00
Paul Bottein b08f5bcb34 Add custom card suggestions in the entity card picker (#52228)
* Add custom card suggestions in the entity card picker

* Prettier

* rename function

* Use ensure array
2026-05-27 14:38:53 +02:00
Wendelin c329e5b827 Revert "Automation triggers - auto IDs" (#52226) 2026-05-27 14:19:38 +02:00
Paulus Schoutsen 97f591337d Fix cloud TTS try dialog failing on default browser target (#52231)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 14:17:18 +02:00
Bram Kragten e6e6e75f73 Revert "Add-on iframe: delegate microphone + camera Permissions Policy" (#52229) 2026-05-27 13:08:57 +02:00
Wendelin ff334de0ca Fix checked radio option (#52227) 2026-05-27 12:46:14 +02:00
Bram Kragten 8dbe97b480 Add device step to matter add flow (#52216)
* Add device step to matter add flow

* Update matter-add-device-device-added.ts
2026-05-27 12:48:14 +03:00
Bram Kragten 7bea54851d Remove advanced mode completely (#52212) 2026-05-27 09:20:48 +00:00
Bram Kragten 9298e00f20 Merge branch 'rc' 2026-05-14 11:32:44 +02:00
Bram Kragten 70085d4bad Bumped version to 20260429.4 2026-05-14 11:32:29 +02:00
Wendelin d83a553b62 Reactivate iOS focus element (#52020) 2026-05-14 11:31:23 +02:00
Wendelin cab5c6af30 Add macOS version mapping for Safari 26 support (#51999) 2026-05-14 11:24:44 +02:00
Petar Petrov d44d8a6dbd Fix water sankey untracked consumption with nested sub-trackers (#51998) 2026-05-14 11:24:43 +02:00
karwosts 3cf1d94b92 Fix sensor card when visibility changes (#51953)
* Fix sensor card when visibility changes

* History card

* map card

* trend graph

* minor change
2026-05-14 11:23:31 +02:00
Tom Carpenter 9f5f849e32 Fix demo instance mock recorder data generation (#51950)
Fix demo mock recorder data end times

The mock recorder was setting the start and end time for each of the samples to be the same value, causing the solar graph in the energy dashboard to render incorrectly.

Fix the recorder to set the end time of each sample to the start time of the next.
2026-05-14 11:21:23 +02:00
karwosts 27e9926363 Fix heading badge current-entity visibility (#51942) 2026-05-14 11:21:22 +02:00
karwosts efe734892a Fix create new person with login (#51939) 2026-05-14 11:21:21 +02:00
Tom Carpenter b3d79e312d Remove extra padding to right of ha-switch (#51932)
Fix empty padding to right of ha-switch

When the label slot for the ha-switch is empty, the initial margin is still present which causes an odd misalignment on the switches in e,g, the entities card.

To fix this, if the label slot is empty, hide the label to remove the unwanted margin.
2026-05-14 11:21:19 +02:00
Marcin Bauer ecfef9e112 Improve continue on error tooltip in automation editor (#51926)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-14 11:21:18 +02:00
George Caliment ca960446f0 Fixed blueprint rows event result chip render when collapsed (#51910) 2026-05-14 11:21:17 +02:00
Petar Petrov a6eb722025 Clamp power sources graph usage line to non-negative (#51902) 2026-05-14 11:21:16 +02:00
Paul Bottein f3ff01ace4 Fix race condition loading home dashboard favorites (#51901) 2026-05-14 11:21:15 +02:00
karwosts d5e1a373ec Fix entity filter card (#51895) 2026-05-14 11:21:14 +02:00
Wendelin e1b9a1a185 Fix content padding picker (#51889) 2026-05-14 11:21:12 +02:00
Paul Bottein efe8eaa941 Move logs page search bar out of the toolbar (#51887) 2026-05-14 11:21:11 +02:00
Wendelin 5856196ef3 Improve automation event chips action, condition (#51886) 2026-05-14 11:21:10 +02:00
Clément Notin 2671a8c64b Fix quick bar search not focused on first open (#51822)
Co-authored-by: Wendelin <12148533+wendevlin@users.noreply.github.com>
2026-05-14 11:21:09 +02:00
Bram Kragten 8620653a54 Merge branch 'rc' 2026-05-06 11:19:42 +02:00
Bram Kragten c4f4cbd323 Bumped version to 20260429.3 2026-05-06 11:18:01 +02:00
Paul Bottein 2e0df00f0f Fix name for battery entities without device (#51879) 2026-05-06 11:17:09 +02:00
Wendelin ce02f8072d Reduce progress bar default height (#51878)
reduce progress bar default height to 12px
2026-05-06 11:17:08 +02:00
Paul Bottein c973aa7516 Fix media controls in media player more info dialog (#51877) 2026-05-06 11:17:07 +02:00
Paul Bottein 1e2328707c Fix switch clipping in view visibility editor (#51876) 2026-05-06 11:17:06 +02:00
Wendelin 56368b88cd Remove duplicate definition in semantic colors (#51875)
* Remove duplicate definition in semantic colors

* rearrange surface tokens
2026-05-06 11:17:05 +02:00
Aidan Timson fcd4f177c1 Fix Safari 14 legacy bundle require errors (#51868)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 11:17:04 +02:00
Wendelin 7423ae7316 Fix integration search shrink on mobile (#51867) 2026-05-06 11:17:03 +02:00
Marcin Bauer 4427c581f1 Fix automation row right padding and soften chip highlight animation (#51865)
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-06 11:17:02 +02:00
Paul Bottein cf86bb9821 Use ha-switch instead of ha-control-switch in entity toggle (#51852) 2026-05-06 11:17:01 +02:00
karwosts 897802dc16 Change display for uptime sensors (#51830) 2026-05-06 11:17:00 +02:00
Paul Bottein 95edd6c2c2 20260429.2 (#51856) 2026-05-04 17:05:24 +02:00
Paul Bottein dd65173c5a Bumped version to 20260429.2 2026-05-04 17:04:06 +02:00
Paul Bottein cf26753f7d Remove daily and hourly forecast card features (#51854) 2026-05-04 17:03:54 +02:00
Paul Bottein d6ab8ffb16 Resolve service name and icon for shortcut card and badge (#51850) 2026-05-04 17:03:53 +02:00
Wendelin 2dc4b16eac Fix automation row target width (#51848) 2026-05-04 17:03:52 +02:00
Paul Bottein 1eba765bc2 Group areas floor vacuum clean (#51847) 2026-05-04 17:03:51 +02:00
Wendelin 398479ddd7 Use ha-switch in ha-automation-picker (#51846)
use ha-switch in ha-automation-picker
2026-05-04 17:03:50 +02:00
Paul Bottein c4fd7bb3e1 Fix entity toggle switch size (#51845) 2026-05-04 17:03:49 +02:00
Isaac (Kwangjin Ko) 4cfc67a95e ha-humidifier-state: fix incorrect translation key for 'Currently' (#51843) 2026-05-04 17:03:48 +02:00
Brooke Hatton e38d1964ca Remove battery chargers from maintenance dashboard (#51835) 2026-05-04 17:03:47 +02:00
Paul Bottein ec8b5c77bd Add min touch size for control switch (#51826) 2026-05-04 17:03:46 +02:00
Simon Lamon 425f2775e2 Missing toggle in switch group (#51825)
Missing toggle
2026-05-04 17:03:45 +02:00
Brooke Hatton 3a3d8191a3 Adjust Copy for maintenance summary card and include unavailable device count (#51815)
* Adjust Copy For summary card

* Further tweak copy and include unavailable devices
2026-05-04 17:03:44 +02:00
Aidan Timson 04fca68549 Add gap between hui editors and previews on mobile (#51811) 2026-05-04 17:03:43 +02:00
Paul Bottein 3046f3e47d 20260429.1 (#51817) 2026-04-30 20:33:33 +02:00
Paul Bottein 35601a0900 Bumped version to 20260429.1 2026-04-30 20:32:28 +02:00
Wendelin e7016c15af Fix ha-select undefined value (#51800)
Fix ha-select undefined

Co-authored-by: Copilot <copilot@github.com>
2026-04-30 20:32:08 +02:00
Wendelin 624521e30b Hide tooltip on mobile clients in ha-sidebar component (#51799) 2026-04-30 20:32:08 +02:00
Bram Kragten 4876bfa639 Add tooltips for Jinja editors (#51792)
* Add descriptions to Jinja2 tags, filters, expressions, tests and variables

All standard Jinja2 tags, filters, and expression completions now carry
info and detail strings so the autocomplete info popover shows meaningful
documentation when users browse them — not just HA-specific functions.

* Add keyboard shortcut tip to the template developer tool

A ha-tip below the editor card now shows users that Ctrl+Space triggers
autocomplete, Ctrl+F opens the search panel, and F11 toggles fullscreen,
making the editor's built-in features more discoverable.

* Add hover tooltips for Jinja2 functions, filters and expressions

Hovering over a function, filter, tag, test, or variable name inside a
Jinja2 template shows a tooltip with its signature and description.
Non-tag completions also get a help-circle icon linking to the
corresponding Home Assistant template-functions documentation page.

The tooltip is rendered as a custom Lit element (ha-code-editor-jinja-hover)
that takes the Completion object and an optional docUrl as properties.

The tooltip source (haJinjaHoverSource) is wired into ha-code-editor
via CodeMirror's hoverTooltip extension. The documentationUrl() helper
is used so the link points to the correct subdomain (www / rc / next)
based on the running HA version.

* Add hover tooltips for Jinja2 hover + arg value tooltips for entity/device/area

Wire haJinjaHoverSource into ha-code-editor via CodeMirror hoverTooltip.
Two types of hover are now shown in jinja2/yaml mode:

- Hovering a function/filter/tag/expression name shows its signature,
  description, and a doc-link icon (non-tags only).
- Hovering a string-literal argument of a known HA Jinja function (e.g.
  states(), device_name(), area_entities()) shows the friendly name,
  current state, device, and area for entity_id arguments; the device
  name and area for device_id arguments; and the area name for area_id
  arguments. The same applies to states["entity_id"] subscripts.

The arg-value tooltip reuses CompletionItem / ha-code-editor-completion-items
(the same component used for autocomplete info popovers) via a new
ha-code-editor-jinja-arg-hover element. HA registry data is passed from
ha-code-editor via a HassArgHoverContext interface to keep jinja_ha_completions.ts
free of HomeAssistant type imports.

* only add tip for autocomplete

* review
2026-04-30 20:32:07 +02:00
AlCalzone 5dea0764b2 Expose Z-Wave exclusion instructions when removing device (#51788)
* Expose Z-Wave exclusion instructions when removing device

* text tweaks

* Apply suggestion from @MindFreeze

* Apply suggestions from code review

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>

* bring back comment

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-04-30 20:32:05 +02:00
Paul Bottein 121ed7ac1f 20260429.0 (#51790) 2026-04-29 16:47:32 +02:00
165 changed files with 3528 additions and 2649 deletions

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "home-assistant-frontend"
version = "20260429.0"
version = "20260527.3"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"
+13
View File
@@ -17,6 +17,19 @@ export interface NavigateOptions {
// max time to wait for dialogs to close before navigating
const DIALOG_WAIT_TIMEOUT = 500;
/**
* Stash a destination URL in the current history entry's state. If the page
* is refreshed while a dialog is open, urlSyncMixin will navigate to this URL
* on load instead of cleaning up the stale dialog state by going back.
* The current URL is not changed.
*/
export const setRefreshUrl = (path: string) => {
mainWindow.history.replaceState(
{ ...mainWindow.history.state, refreshUrl: path },
""
);
};
/**
* Ensures all dialogs are closed before navigation.
* Returns true if navigation can proceed, false if a dialog refused to close.
@@ -33,30 +33,32 @@ export class HaAutomationRowLiveTest extends LitElement {
static styles = css`
:host {
position: absolute;
top: -5px;
inset-inline-end: -6px;
display: inline-block;
}
#indicator {
width: 12px;
height: 12px;
width: 10px;
height: 10px;
border-radius: var(--ha-border-radius-circle);
border: 3px solid;
border: var(--ha-border-width-md) solid;
box-sizing: border-box;
background-color: var(--card-background-color);
box-shadow: 0 0 0 2px var(--card-background-color);
transition: all var(--ha-animation-duration-normal) ease-in-out;
}
:host([state="pass"]) #indicator {
background-color: var(--ha-color-fill-success-loud-resting);
border-color: var(--ha-color-fill-success-loud-resting);
background-color: var(--ha-color-green-60);
border-color: var(--ha-color-green-60);
}
:host([state="fail"]) #indicator {
border-color: var(--ha-color-fill-warning-loud-resting);
border-color: var(--ha-color-orange-60);
}
:host([state="invalid"]) #indicator {
border-color: var(--ha-color-fill-danger-loud-resting);
border-color: var(--ha-color-red-60);
}
:host([state="unknown"]) #indicator {
border-color: var(--ha-color-fill-neutral-loud-resting);
border-color: var(--ha-color-neutral-60);
}
`;
}
@@ -165,7 +165,7 @@ export class HaAutomationRow extends LitElement {
::slotted([slot="leading-icon"]) {
color: var(--ha-color-on-neutral-quiet);
}
:host([building-block]) ::slotted([slot="leading-icon"]) {
:host([building-block]) ::slotted(#condition-icon) {
--mdc-icon-size: var(--ha-space-5);
color: var(--white-color);
transform: rotate(-45deg);
+82 -47
View File
@@ -14,6 +14,7 @@ import type {
ECElementEvent,
LegendComponentOption,
LineSeriesOption,
TooltipOption,
XAXisOption,
YAXisOption,
} from "echarts/types/dist/shared";
@@ -29,22 +30,59 @@ import type { HASSDomEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { listenMediaQuery } from "../../common/dom/media_query";
import { afterNextRender } from "../../common/util/render-status";
import { filterXSS } from "../../common/util/xss";
import { uiContext } from "../../data/context";
import type { Themes } from "../../data/ws-themes";
import type { ECOption } from "../../resources/echarts/echarts";
import type {
ECOption,
HaECOption,
HaECSeries,
HaECSeriesItem,
HaTooltipOption,
} from "../../resources/echarts/echarts";
import type { HomeAssistant, HomeAssistantUI } from "../../types";
import { isMac } from "../../util/is_mac";
import "../chips/ha-assist-chip";
import "../ha-icon-button";
import { formatTimeLabel } from "./axis-label";
import { downSampleLineData } from "./down-sample";
import { wrapLitTooltipFormatter } from "./lit-tooltip-formatter";
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
const LEGEND_OVERFLOW_LIMIT = 10;
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
const DOUBLE_TAP_TIME = 300;
type RawSeriesOption = Exclude<
NonNullable<ECOption["series"]>,
readonly unknown[]
>;
const toEChartsFormatter = (
fn: ReturnType<typeof wrapLitTooltipFormatter>
): NonNullable<TooltipOption["formatter"]> =>
fn as NonNullable<TooltipOption["formatter"]>;
const convertHaTooltipFormatter = (tooltip: HaTooltipOption): TooltipOption => {
const { formatter, ...rest } = tooltip;
const next: TooltipOption = { ...rest };
if (typeof formatter === "function") {
next.formatter = toEChartsFormatter(wrapLitTooltipFormatter(formatter));
} else if (formatter !== undefined) {
next.formatter = formatter;
}
return next;
};
const processSeriesTooltipFormatter = (s: HaECSeriesItem): RawSeriesOption => {
if (s.tooltip && typeof s.tooltip.formatter === "function") {
return {
...s,
tooltip: convertHaTooltipFormatter(s.tooltip),
} as RawSeriesOption;
}
return s as RawSeriesOption;
};
export type CustomLegendOption = ECOption["legend"] & {
type: "custom";
data?: {
@@ -66,9 +104,9 @@ export class HaChartBase extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public data: ECOption["series"] = [];
@property({ attribute: false }) public data: HaECSeries = [];
@property({ attribute: false }) public options?: ECOption;
@property({ attribute: false }) public options?: HaECOption;
@property({ type: String }) public height?: string;
@@ -614,7 +652,7 @@ export class HaChartBase extends LitElement {
// Return an array of all IDs associated with the legend item of the primaryId
private _getAllIdsFromLegend(
options: ECOption | undefined,
options: HaECOption | undefined,
primaryId: string
): string[] {
if (!options) return [primaryId];
@@ -634,7 +672,7 @@ export class HaChartBase extends LitElement {
// Parses the options structure and adds all ids of unselected legend items to hiddenDatasets.
// No known need to remove items at this time.
private _updateHiddenStatsFromOptions(options: ECOption | undefined) {
private _updateHiddenStatsFromOptions(options: HaECOption | undefined) {
if (!options) return;
const legend = ensureArray(this.options?.legend || [])[0] as
| LegendComponentOption
@@ -757,22 +795,34 @@ export class HaChartBase extends LitElement {
xAxis,
};
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
if (isMobile && options.tooltip) {
// mobile charts are full width so we need to confine the tooltip to the chart
const tooltips = Array.isArray(options.tooltip)
? options.tooltip
: [options.tooltip];
tooltips.forEach((tooltip) => {
tooltip.confine = true;
tooltip.appendTo = undefined;
tooltip.triggerOn = "click";
});
options.tooltip = tooltips;
if (options.tooltip) {
const isMobile = window.matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
).matches;
// Shallow-copy each tooltip object so wrap/mobile mutations don't leak
// back into the caller's options.tooltip reference (callers may cache the
// options object via memoizeOne, in which case in-place mutation would
// pollute that cache across chart instances).
const processTooltip = (tooltip: HaTooltipOption): TooltipOption => {
const next = convertHaTooltipFormatter(tooltip);
if (isMobile) {
// mobile charts are full width so we need to confine the tooltip to the chart
next.confine = true;
next.appendTo = undefined;
next.triggerOn = "click";
}
return next;
};
const haTooltip = options.tooltip;
const processedTooltip = Array.isArray(haTooltip)
? haTooltip.map(processTooltip)
: processTooltip(haTooltip);
return {
...options,
tooltip: processedTooltip,
} as ECOption;
}
return options;
return options as ECOption;
}
private _createTheme(style: CSSStyleDeclaration) {
@@ -956,30 +1006,16 @@ export class HaChartBase extends LitElement {
const xAxis = (this.options?.xAxis?.[0] ?? this.options?.xAxis) as
| XAXisOption
| undefined;
const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as
| YAXisOption
| undefined;
const series = ensureArray(this.data).map((s) => {
const data = this._hiddenDatasets.has(String(s.id ?? s.name))
? undefined
: s.data;
let result = {
...s,
data,
} as HaECSeriesItem;
if (data && s.type === "line") {
if (yAxis?.type === "log") {
// set <=0 values to null so they render as gaps on a log graph
return {
...s,
data: (data as LineSeriesOption["data"])!.map((v) =>
Array.isArray(v)
? [
v[0],
typeof v[1] !== "number" || v[1] > 0 ? v[1] : null,
...v.slice(2),
]
: v
),
};
}
if (s.sampling === "minmax") {
if ((s as LineSeriesOption).sampling === "minmax") {
const minX = xAxis?.min
? xAxis.min instanceof Date
? xAxis.min.getTime()
@@ -994,8 +1030,8 @@ export class HaChartBase extends LitElement {
? xAxis.max
: undefined
: undefined;
return {
...s,
result = {
...result,
sampling: undefined,
data: downSampleLineData(
data as LineSeriesOption["data"],
@@ -1003,11 +1039,10 @@ export class HaChartBase extends LitElement {
minX,
maxX
),
};
} as HaECSeriesItem;
}
}
const name = filterXSS(String(s.name ?? s.id ?? ""));
return { ...s, name, data };
return processSeriesTooltipFormatter(result);
});
return series as ECOption["series"];
}
@@ -1344,8 +1379,8 @@ export class HaChartBase extends LitElement {
}
private _compareCustomLegendOptions(
oldOptions: ECOption | undefined,
newOptions: ECOption | undefined
oldOptions: HaECOption | undefined,
newOptions: HaECOption | undefined
): boolean {
const oldLegends = ensureArray(
oldOptions?.legend || []
@@ -0,0 +1,41 @@
import type { PropertyValues } from "lit";
import { css, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
@customElement("ha-chart-tooltip-marker")
class HaChartTooltipMarker extends LitElement {
@property() public color = "";
@property({ type: Boolean, reflect: true }) public rtl = false;
protected willUpdate(changed: PropertyValues) {
if (changed.has("color")) {
this.style.backgroundColor = this.color;
}
}
protected render() {
return nothing;
}
static styles = css`
:host {
display: inline-block;
margin-inline-end: 4px;
margin-inline-start: initial;
border-radius: 10px;
width: 10px;
height: 10px;
vertical-align: middle;
}
:host([rtl]) {
direction: rtl;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-chart-tooltip-marker": HaChartTooltipMarker;
}
}
+4 -4
View File
@@ -1,6 +1,6 @@
import type { EChartsType } from "echarts/core";
import type { GraphSeriesOption } from "echarts/charts";
import type { PropertyValues } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state, query } from "lit/decorators";
@@ -11,7 +11,7 @@ import type {
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
import memoizeOne from "memoize-one";
import { listenMediaQuery } from "../../common/dom/media_query";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HaECOption } from "../../resources/echarts/echarts";
import "./ha-chart-base";
import type { HaChartBase } from "./ha-chart-base";
import type { HomeAssistant } from "../../types";
@@ -78,7 +78,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public tooltipFormatter?: (
params: TopLevelFormatterParams
) => string;
) => TemplateResult | typeof nothing | null;
/**
* Optional callback that returns additional searchable strings for a node.
@@ -182,7 +182,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
}
private _createOptions = memoizeOne(
(categories?: NetworkData["categories"]): ECOption => ({
(categories?: NetworkData["categories"]): HaECOption => ({
tooltip: {
trigger: "item",
confine: true,
+14 -6
View File
@@ -11,10 +11,10 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
import { fireEvent } from "../../common/dom/fire_event";
import SankeyChart from "../../resources/echarts/components/sankey/install";
import type { HomeAssistant } from "../../types";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HaECOption } from "../../resources/echarts/echarts";
import { measureTextWidth } from "../../util/text";
import { filterXSS } from "../../common/util/xss";
import "./ha-chart-base";
import "./ha-chart-tooltip-marker";
import { NODE_SIZE } from "../trace/hat-graph-const";
import "../ha-alert";
@@ -71,7 +71,7 @@ export class HaSankeyChart extends LitElement {
});
render() {
const options = {
const options: HaECOption = {
grid: {
top: 0,
bottom: 0,
@@ -83,7 +83,7 @@ export class HaSankeyChart extends LitElement {
formatter: this._renderTooltip,
appendTo: document.body,
},
} as ECOption;
};
return html`<ha-chart-base
.hass=${this.hass}
@@ -101,14 +101,22 @@ export class HaSankeyChart extends LitElement {
const value = this.valueFormatter
? this.valueFormatter(data.value)
: data.value;
// Keep numbers and units left-to-right, even in RTL locales.
const formattedValue = html`<div style="direction:ltr; display: inline;">
${value}
</div>`;
if (data.id) {
const node = this.data.nodes.find((n) => n.id === data.id);
return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`;
return html`<ha-chart-tooltip-marker
.color=${String(params.color ?? "")}
></ha-chart-tooltip-marker>
${node?.label ?? data.id}<br />${formattedValue}`;
}
if (data.source && data.target) {
const source = this.data.nodes.find((n) => n.id === data.source);
const target = this.data.nodes.find((n) => n.id === data.target);
return `${filterXSS(source?.label ?? data.source)} ${filterXSS(target?.label ?? data.target)}<br>${value}`;
return html`${source?.label ?? data.source}
${target?.label ?? data.target}<br />${formattedValue}`;
}
return null;
};
+8 -5
View File
@@ -5,10 +5,10 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { getGraphColorByIndex } from "../../common/color/colors";
import { filterXSS } from "../../common/util/xss";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HaECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import "./ha-chart-base";
import "./ha-chart-tooltip-marker";
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
let SunburstChart: typeof import("echarts/lib/chart/sunburst/install");
@@ -50,13 +50,13 @@ export class HaSunburstChart extends LitElement {
return nothing;
}
const options = {
const options: HaECOption = {
tooltip: {
trigger: "item",
formatter: this._renderTooltip,
appendTo: document.body,
},
} as ECOption;
};
return html`<ha-chart-base
.data=${this._createData(this.data)}
@@ -71,7 +71,10 @@ export class HaSunburstChart extends LitElement {
const value = this.valueFormatter
? this.valueFormatter(data.value)
: data.value;
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
return html`<ha-chart-tooltip-marker
.color=${String(params.color ?? "")}
></ha-chart-tooltip-marker>
${data.name}<br />${value}`;
};
private _createData = memoizeOne(
@@ -0,0 +1,41 @@
import { nothing, render } from "lit";
import type { LitTooltipFormatter } from "../../resources/echarts/echarts";
type WrappedTooltipFormatter = (
params: unknown,
ticket?: string
) => HTMLElement | null;
export type { WrappedTooltipFormatter };
const litTooltipFormatterCache = new WeakMap<
LitTooltipFormatter | WrappedTooltipFormatter,
WrappedTooltipFormatter
>();
export const wrapLitTooltipFormatter = (
fn: LitTooltipFormatter | WrappedTooltipFormatter
): WrappedTooltipFormatter => {
const cached = litTooltipFormatterCache.get(fn);
if (cached) return cached;
const container = document.createElement("div");
// display:contents keeps the wrapper layout-invisible so its children act as
// direct children of echarts' tooltip box, matching the prior innerHTML behavior.
container.style.display = "contents";
const wrapped: WrappedTooltipFormatter = (params, ticket) => {
const result = (fn as LitTooltipFormatter)(params, ticket);
// `nothing` and null/undefined must all suppress the tooltip. Returning
// `nothing` to echarts via `render(nothing, container)` leaves a Lit
// comment marker behind so echarts would show an empty box; convert it to
// null instead so `setContent(null)` clears innerHTML and `show()` hides.
if (result === null || result === undefined || result === nothing) {
return null;
}
render(result, container);
return container;
};
litTooltipFormatterCache.set(fn, wrapped);
// Idempotent re-wrap: looking up the wrapped fn returns itself.
litTooltipFormatterCache.set(wrapped, wrapped);
return wrapped;
};
@@ -1,5 +1,5 @@
import type { PropertyValues } from "lit";
import { html, LitElement } from "lit";
import type { PropertyValues, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type { VisualMapComponentOption } from "echarts/components";
import type { LineSeriesOption } from "echarts/charts";
@@ -12,8 +12,9 @@ import type { LineChartEntity, LineChartState } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import "./ha-chart-tooltip-marker";
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HaECOption } from "../../resources/echarts/echarts";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
getNumberFormatOptions,
@@ -24,7 +25,6 @@ import type { HASSDomEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
import { filterXSS } from "../../common/util/xss";
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
const safeParseFloat = (value) => {
@@ -108,7 +108,7 @@ export class StateHistoryChartLine extends LitElement {
private _datasetToDataIndex: number[] = [];
@state() private _chartOptions?: ECOption;
@state() private _chartOptions?: HaECOption;
private _hiddenStats = new Set<string>();
@@ -141,12 +141,11 @@ export class StateHistoryChartLine extends LitElement {
private _renderTooltip = (params: any) => {
const time = params[0].axisValue;
const title =
formatDateTimeWithSeconds(
new Date(time),
this.hass.locale,
this.hass.config
) + "<br>";
const title = formatDateTimeWithSeconds(
new Date(time),
this.hass.locale,
this.hass.config
);
const datapoints: Record<string, any>[] = [];
this._chartData.forEach((dataset, index) => {
if (
@@ -177,52 +176,44 @@ export class StateHistoryChartLine extends LitElement {
seriesName: dataset.name,
seriesIndex: index,
value: lastData,
// HTML copied from echarts. May change based on options
marker: `<span style="display:inline-block;margin-right:4px;margin-inline-end:4px;margin-inline-start:initial;border-radius:10px;width:10px;height:10px;background-color:${dataset.color};"></span>`,
color: dataset.color,
});
});
const unit = this.unit
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
: "";
return (
title +
datapoints
.map((param) => {
const entityId = this._entityIds[param.seriesIndex];
const stateObj = this.hass.states[entityId];
const entry = this.hass.entities[entityId];
const stateValue = String(param.value[1]);
let value = stateObj
? this.hass.formatEntityState(stateObj, stateValue)
: `${formatNumber(
stateValue,
this.hass.locale,
getNumberFormatOptions(undefined, entry)
)}${unit}`;
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
const data = this.data[dataIndex];
if (data.statistics && data.statistics.length > 0) {
value += "<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
const source =
data.states.length === 0 ||
param.value[0] < data.states[0].last_changed
? `${this.hass.localize(
"ui.components.history_charts.source_stats"
)}`
: `${this.hass.localize(
"ui.components.history_charts.source_history"
)}`;
value += source;
}
if (param.seriesName) {
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
}
return `${param.marker} ${value}`;
})
.join("<br>")
);
return html`${title}${datapoints.map((param) => {
const entityId = this._entityIds[param.seriesIndex];
const stateObj = this.hass.states[entityId];
const entry = this.hass.entities[entityId];
const stateValue = String(param.value[1]);
const value = stateObj
? this.hass.formatEntityState(stateObj, stateValue)
: `${formatNumber(
stateValue,
this.hass.locale,
getNumberFormatOptions(undefined, entry)
)}${unit}`;
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
const data = this.data[dataIndex];
let statSuffix: TemplateResult | typeof nothing = nothing;
if (data.statistics && data.statistics.length > 0) {
const source =
data.states.length === 0 ||
param.value[0] < data.states[0].last_changed
? this.hass.localize("ui.components.history_charts.source_stats")
: this.hass.localize("ui.components.history_charts.source_history");
// Five non-breaking spaces indent the source label.
statSuffix = html`<br />${"\u00a0".repeat(5)}${source}`;
}
return html`<br /><ha-chart-tooltip-marker
.color=${String(param.color ?? "")}
></ha-chart-tooltip-marker>
${param.seriesName
? html`${param.seriesName}: `
: nothing}${value}${statSuffix}`;
})}`;
};
private _datasetHidden(ev: CustomEvent) {
@@ -1,11 +1,10 @@
import type { PropertyValues } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type {
CustomSeriesOption,
CustomSeriesRenderItem,
ECElementEvent,
TooltipFormatterCallback,
TooltipPositionCallbackParams,
} from "echarts/types/dist/shared";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
@@ -15,8 +14,9 @@ import type { TimelineEntity } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import "./ha-chart-tooltip-marker";
import { computeTimelineColor } from "./timeline-color";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
import echarts from "../../resources/echarts/echarts";
import { luminosity } from "../../common/color/rgb";
import { hex2rgb } from "../../common/color/convert-color";
@@ -57,7 +57,7 @@ export class StateHistoryChartTimeline extends LitElement {
@state() private _chartData: CustomSeriesOption[] = [];
@state() private _chartOptions?: ECOption;
@state() private _chartOptions?: HaECOption;
@state() private _yWidth = 0;
@@ -69,7 +69,7 @@ export class StateHistoryChartTimeline extends LitElement {
.hass=${this.hass}
.options=${this._chartOptions}
.height=${`${this.data.length * 30 + 30}px`}
.data=${this._chartData as ECOption["series"]}
.data=${this._chartData as HaECSeries}
small-controls
@chart-click=${this._handleChartClick}
@chart-zoom=${this._handleDataZoom}
@@ -132,42 +132,35 @@ export class StateHistoryChartTimeline extends LitElement {
return rect;
};
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
(params: TooltipPositionCallbackParams) => {
const { value, name, marker, seriesName, color } = Array.isArray(params)
? params[0]
: params;
const title = seriesName
? `<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
: "";
const durationInMs = value![2] - value![1];
const formattedDuration = `${this.hass.localize(
"ui.components.history_charts.duration"
)}: ${millisecondsToDuration(durationInMs)}`;
private _renderTooltip = (params: TooltipPositionCallbackParams) => {
const { value, name, seriesName, color } = Array.isArray(params)
? params[0]
: params;
const durationInMs = value![2] - value![1];
const formattedDuration = `${this.hass.localize(
"ui.components.history_charts.duration"
)}: ${millisecondsToDuration(durationInMs)}`;
const markerLocalized = !computeRTL(
this.hass.language,
this.hass.translationMetadata.translations
)
? marker
: `<span style="direction: rtl;display:inline-block;margin-right:4px;margin-inline-end:4px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
const lines = [
markerLocalized + name,
formatDateTimeWithSeconds(
new Date(value![1]),
this.hass.locale,
this.hass.config
),
formatDateTimeWithSeconds(
new Date(value![2]),
this.hass.locale,
this.hass.config
),
formattedDuration,
].join("<br>");
return [title, lines].join("");
};
const rtl = computeRTL(
this.hass.language,
this.hass.translationMetadata.translations
);
return html`${seriesName
? html`<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
: nothing}<ha-chart-tooltip-marker
.color=${String(color ?? "")}
.rtl=${rtl}
></ha-chart-tooltip-marker
>${name}<br />${formatDateTimeWithSeconds(
new Date(value![1]),
this.hass.locale,
this.hass.config
)}<br />${formatDateTimeWithSeconds(
new Date(value![2]),
this.hass.locale,
this.hass.config
)}<br />${formattedDuration}`;
};
public willUpdate(changedProps: PropertyValues) {
if (
+93 -82
View File
@@ -4,7 +4,7 @@ import type {
ZRColor,
} from "echarts/types/dist/shared";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
@@ -34,12 +34,13 @@ import {
isExternalStatistic,
statisticsHaveType,
} from "../../data/recorder";
import type { ECOption } from "../../resources/echarts/echarts";
import type { HaECOption } from "../../resources/echarts/echarts";
import type { HomeAssistant } from "../../types";
import { getPeriodicAxisLabelConfig } from "./axis-label";
import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import "./ha-chart-tooltip-marker";
import { fillDataGapsAndRoundCaps } from "./round-caps";
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
@@ -126,7 +127,7 @@ export class StatisticsChart extends LitElement {
@state() private _statisticIds: string[] = [];
@state() private _chartOptions?: ECOption;
@state() private _chartOptions?: HaECOption;
@state() private _hiddenStats = new Set<string>();
@@ -251,91 +252,101 @@ export class StatisticsChart extends LitElement {
const unit = this.unit
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
: "";
return params
.map((param, index: number) => {
if (rendered[param.seriesIndex]) return "";
rendered[param.seriesIndex] = true;
const rows: {
time?: string;
color: string;
seriesName?: string;
value: string;
}[] = [];
for (const param of params) {
if (rendered[param.seriesIndex]) continue;
rendered[param.seriesIndex] = true;
const statisticId = this._statisticIds[param.seriesIndex];
const stateObj = this.hass.states[statisticId];
const entry = this.hass.entities[statisticId];
let rawValue: string;
let rawTime: string;
if (chartIsBar) {
// For bar charts value is always second value.
rawValue = String(param.value[1]);
// Time value is third value (un-shifted date) if given, otherwise first value
let startTime: Date;
let endTime: Date | undefined;
if (param.value[2]) {
startTime = new Date(param.value[2]);
if (param.value[3]) {
endTime = new Date(param.value[3]);
}
} else {
startTime = new Date(param.value[0]);
}
if (
period === "year" ||
period === "month" ||
period === "week" ||
period === "day"
) {
// For year/month/day periods, show only the date
rawTime =
formatDate(startTime, this.hass.locale, this.hass.config) +
(endTime && period !== "day"
? ` ${formatDate(
endTime,
this.hass.locale,
this.hass.config
)}`
: "") +
"<br>";
} else {
// For other time periods, include time in render, and optionally show range
// if we have an end time.
rawTime =
formatDateTimeWithSeconds(
startTime,
this.hass.locale,
this.hass.config
) +
(endTime
? ` ${formatTimeWithSeconds(
endTime,
this.hass.locale,
this.hass.config
)}`
: "") +
"<br>";
const statisticId = this._statisticIds[param.seriesIndex];
const stateObj = this.hass.states[statisticId];
const entry = this.hass.entities[statisticId];
let rawValue: string;
let rawTime: string;
if (chartIsBar) {
// For bar charts value is always second value.
rawValue = String(param.value[1]);
// Time value is third value (un-shifted date) if given, otherwise first value
let startTime: Date;
let endTime: Date | undefined;
if (param.value[2]) {
startTime = new Date(param.value[2]);
if (param.value[3]) {
endTime = new Date(param.value[3]);
}
} else {
// For lines max series can have 3 values, as the second value is the max-min to form a band
rawValue = String(param.value[2] ?? param.value[1]);
// Time value is always first value
rawTime = `${formatDateTimeWithSeconds(
new Date(param.value[0]),
this.hass.locale,
this.hass.config
)} <br>`;
startTime = new Date(param.value[0]);
}
const options = getNumberFormatOptions(stateObj, entry) ?? {
maximumFractionDigits: 2,
};
const value = `${formatNumber(
rawValue,
if (
period === "year" ||
period === "month" ||
period === "week" ||
period === "day"
) {
// For year/month/day periods, show only the date
rawTime =
formatDate(startTime, this.hass.locale, this.hass.config) +
(endTime && period !== "day"
? ` ${formatDate(endTime, this.hass.locale, this.hass.config)}`
: "");
} else {
// For other time periods, include time in render, and optionally show range
// if we have an end time.
rawTime =
formatDateTimeWithSeconds(
startTime,
this.hass.locale,
this.hass.config
) +
(endTime
? ` ${formatTimeWithSeconds(
endTime,
this.hass.locale,
this.hass.config
)}`
: "");
}
} else {
// For lines max series can have 3 values, as the second value is the max-min to form a band
rawValue = String(param.value[2] ?? param.value[1]);
// Time value is always first value
rawTime = formatDateTimeWithSeconds(
new Date(param.value[0]),
this.hass.locale,
options
)}${unit}`;
this.hass.config
);
}
const time = index === 0 ? rawTime : "";
return `${time}${param.marker} ${param.seriesName}: ${value}`;
})
.filter(Boolean)
.join("<br>");
const options = getNumberFormatOptions(stateObj, entry) ?? {
maximumFractionDigits: 2,
};
const value = `${formatNumber(rawValue, this.hass.locale, options)}${unit}`;
rows.push({
time: rows.length === 0 ? rawTime : undefined,
color: String(param.color ?? ""),
seriesName: param.seriesName,
value,
});
}
if (rows.length === 0) return nothing;
return html`${rows.map(
(row, i) =>
html`${row.time
? html`${row.time}<br />`
: nothing}<ha-chart-tooltip-marker
.color=${row.color}
></ha-chart-tooltip-marker>
${row.seriesName}:
${row.value}${i < rows.length - 1 ? html`<br />` : nothing}`
)}`;
};
private _createOptions() {
+6 -3
View File
@@ -6,8 +6,9 @@ import {
mdiInformationOutline,
} from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../common/translations/localize";
import { fireEvent } from "../common/dom/fire_event";
import "./ha-icon-button";
@@ -39,7 +40,9 @@ class HaAlert extends LitElement {
@property({ type: Boolean }) public dismissable = false;
@property({ attribute: false }) public localize?: LocalizeFunc;
@state()
@consumeLocalize()
private _localize?: LocalizeFunc;
@property({ type: Boolean }) public narrow = false;
@@ -68,7 +71,7 @@ class HaAlert extends LitElement {
${this.dismissable
? html`<ha-icon-button
@click=${this._dismissClicked}
.label=${this.localize!("ui.common.dismiss_alert")}
.label=${this._localize?.("ui.common.dismiss_alert")}
.path=${mdiClose}
></ha-icon-button>`
: nothing}
+1
View File
@@ -20,6 +20,7 @@ export class HaCheckListItem extends CheckListItemBase {
separateCheckboxClick = false;
async onChange(event) {
event.stopPropagation();
super.onChange(event);
fireEvent(this, event.type);
}
+1 -1
View File
@@ -77,7 +77,7 @@ export class HaGenericPicker extends PickerMixin(LitElement) {
| "bottom-start"
| "bottom-end"
| "left-start"
| "left-end" = "bottom-start";
| "left-end" = "bottom";
/** If set picker shows an add button instead of textbox when value isn't set */
@property({ attribute: "add-button-label" }) public addButtonLabel?: string;
-1
View File
@@ -121,7 +121,6 @@ export class HaIconPicker extends LitElement {
.label=${this.label}
.value=${this._value}
.searchFn=${this._filterIcons}
popover-placement="bottom-start"
@value-changed=${this._valueChanged}
>
<slot name="start"></slot>
-1
View File
@@ -152,7 +152,6 @@ export class HaLanguagePicker extends LitElement {
<ha-generic-picker
.hass=${this.hass}
.autofocus=${this.autofocus}
popover-placement="bottom-end"
.notFoundLabel=${this._notFoundLabel}
.emptyLabel=${this.hass?.localize(
"ui.components.language-picker.no_languages"
+15 -8
View File
@@ -1,11 +1,12 @@
import { consume, type ContextType } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
import { fireEvent } from "../common/dom/fire_event";
import { computeRTL } from "../common/util/compute_rtl";
import type { HomeAssistant } from "../types";
import { internationalizationContext, uiContext } from "../data/context";
import "./radio/ha-radio-group";
import type { HaRadioGroup } from "./radio/ha-radio-group";
import "./radio/ha-radio-option";
@@ -26,8 +27,6 @@ export interface SelectBoxOption {
@customElement("ha-select-box")
export class HaSelectBox extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public options: SelectBoxOption[] = [];
@property({ attribute: false }) public value?: string;
@@ -40,6 +39,14 @@ export class HaSelectBox extends LitElement {
@property({ type: Boolean, attribute: "stacked_image" })
public stackedImage = false;
@state()
@consume({ context: internationalizationContext, subscribe: true })
protected _i18n?: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: uiContext, subscribe: true })
protected _ui?: ContextType<typeof uiContext>;
render() {
const maxColumns = this.maxColumns ?? 3;
const columns = Math.min(maxColumns, this.options.length);
@@ -62,11 +69,11 @@ export class HaSelectBox extends LitElement {
const disabled = option.disabled || this.disabled || false;
const selected = option.value === this.value;
const isDark = this.hass?.themes.darkMode || false;
const isRTL = this.hass
const isDark = this._ui?.themes.darkMode || false;
const isRTL = this._i18n
? computeRTL(
this.hass.language,
this.hass.translationMetadata.translations
this._i18n.language,
this._i18n.translationMetadata.translations
)
: false;
@@ -1,30 +1,31 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import { fireEvent } from "../../common/dom/fire_event";
import type { LocalizeKeys } from "../../common/translations/localize";
import type {
LocalizeFunc,
LocalizeKeys,
} from "../../common/translations/localize";
import type {
AutomationBehavior,
AutomationBehaviorConditionMode,
AutomationBehaviorSelector,
AutomationBehaviorTriggerMode,
} from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-input-helper-text";
import type { SelectBoxOption } from "../ha-select-box";
import "../ha-select-box";
import type { SelectBoxOption } from "../ha-select-box";
const TRIGGER_BEHAVIORS: AutomationBehaviorTriggerMode[] = [
"any",
"each",
"first",
"last",
"all",
];
const CONDITION_BEHAVIORS: AutomationBehaviorConditionMode[] = ["any", "all"];
@customElement("ha-selector-automation_behavior")
export class HaSelectorAutomationBehavior extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public selector!: AutomationBehaviorSelector;
@@ -39,6 +40,9 @@ export class HaSelectorAutomationBehavior extends LitElement {
@property({ type: Boolean }) public required = true;
@consumeLocalize()
protected _localize?: LocalizeFunc;
protected render() {
const { mode } = this.selector.automation_behavior ?? {};
const modeKey = mode ?? "trigger";
@@ -60,7 +64,6 @@ export class HaSelectorAutomationBehavior extends LitElement {
return html`
<ha-select-box
.hass=${this.hass}
.options=${options}
.value=${this.value ?? ""}
max_columns="1"
@@ -95,8 +98,10 @@ export class HaSelectorAutomationBehavior extends LitElement {
return translated;
}
}
return this.hass.localize(
`ui.components.selectors.automation_behavior.${mode ?? "trigger"}.options.${behavior}.${field}` as LocalizeKeys
return (
this._localize?.(
`ui.components.selectors.automation_behavior.${mode ?? "trigger"}.options.${behavior}.${field}` as LocalizeKeys
) || behavior
);
}
+10 -46
View File
@@ -1,11 +1,10 @@
import { mdiPlayBox, mdiPlus } from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { fireEvent } from "../../common/dom/fire_event";
import { supportsFeature } from "../../common/entity/supports-feature";
import { getSignedPath } from "../../data/auth";
import type { MediaPickedEvent } from "../../data/media-player";
import {
MediaClassBrowserSettings,
@@ -13,14 +12,10 @@ import {
} from "../../data/media-player";
import type { MediaSelector, MediaSelectorValue } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import {
brandsUrl,
extractDomainFromBrandUrl,
isBrandUrl,
} from "../../util/brands-url";
import "../ha-alert";
import "../ha-form/ha-form";
import type { SchemaUnion } from "../ha-form/types";
import "../media-player/ha-media-browser-thumbnail";
import { showMediaBrowserDialog } from "../media-player/show-media-browser-dialog";
import { ensureArray } from "../../common/array/ensure-array";
import "../ha-picture-upload";
@@ -54,8 +49,6 @@ export class HaMediaSelector extends LitElement {
filter_entity?: string | string[];
};
@state() private _thumbnailUrl?: string | null;
private _contextEntities: string[] | undefined;
private get _hasAccept(): boolean {
@@ -68,35 +61,6 @@ export class HaMediaSelector extends LitElement {
this._contextEntities = ensureArray(this.context?.filter_entity);
}
}
if (changedProps.has("value")) {
const thumbnail = this.value?.metadata?.thumbnail;
const oldThumbnail = (changedProps.get("value") as this["value"])
?.metadata?.thumbnail;
if (thumbnail === oldThumbnail) {
return;
}
if (thumbnail && isBrandUrl(thumbnail)) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
this._thumbnailUrl = brandsUrl(
{
domain: extractDomainFromBrandUrl(thumbnail),
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
);
} else if (thumbnail && thumbnail.startsWith("/")) {
this._thumbnailUrl = undefined;
// Thumbnails served by local API require authentication
getSignedPath(this.hass, thumbnail).then((signedPath) => {
this._thumbnailUrl = signedPath.path;
});
} else {
this._thumbnailUrl = thumbnail;
}
}
}
protected render() {
@@ -186,10 +150,12 @@ export class HaMediaSelector extends LitElement {
),
})}
image"
style=${this._thumbnailUrl
? `background-image: url(${this._thumbnailUrl});`
: ""}
></div>
>
<ha-media-browser-thumbnail
.hass=${this.hass}
.url=${this.value.metadata.thumbnail}
></ha-media-browser-thumbnail>
</div>
`
: html`
<div class="icon-holder image">
@@ -410,13 +376,11 @@ export class HaMediaSelector extends LitElement {
right: 0;
left: 0;
bottom: 0;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
--ha-media-browser-thumbnail-fit: cover;
}
.centered-image {
margin: 4px;
background-size: contain;
--ha-media-browser-thumbnail-fit: contain;
}
.icon-holder {
display: flex;
@@ -96,7 +96,6 @@ export class HaSelectSelector extends LitElement {
.value=${this.value as string | undefined}
@value-changed=${this._selectChanged}
.maxColumns=${this.selector.select?.box_max_columns}
.hass=${this.hass}
></ha-select-box>
${this._renderHelper()}
`;
-1
View File
@@ -82,7 +82,6 @@ export class HaThemePicker extends LitElement {
.disabled=${this.disabled}
.required=${this.required}
@value-changed=${this._changed}
popover-placement="bottom"
></ha-generic-picker>
`;
}
+1
View File
@@ -22,6 +22,7 @@ export const haTopAppBarFixedStyles = css`
padding-top: var(--safe-area-inset-top);
padding-right: var(--safe-area-inset-right);
transition:
box-shadow var(--ha-animation-duration-normal) ease,
width var(--ha-animation-duration-normal) ease,
padding-left var(--ha-animation-duration-normal) ease,
padding-right var(--ha-animation-duration-normal) ease;
@@ -0,0 +1,147 @@
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import type { HomeAssistant } from "../../types";
import {
brandsUrl,
extractDomainFromBrandUrl,
isBrandUrl,
} from "../../util/brands-url";
const SMALL_THUMBNAIL_THRESHOLD = 16;
const isSvgUrl = (url: string): boolean =>
/\.svg(\?|#|$)/i.test(url) || url.startsWith("data:image/svg+xml");
const resolveThumbnailURL = (
hass: HomeAssistant,
thumbnailUrl: string
): Promise<string> => {
if (isBrandUrl(thumbnailUrl)) {
return Promise.resolve(
brandsUrl(
{
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
darkOptimized: hass.themes?.darkMode,
},
hass.auth.data.hassUrl
)
);
}
if (thumbnailUrl.startsWith("/")) {
// Local thumbnails require authentication; fetch and inline as base64.
return hass
.fetchWithAuth(thumbnailUrl)
.then((response) => response.blob())
.then(
(blob) =>
new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () =>
resolve(typeof reader.result === "string" ? reader.result : "");
reader.onerror = (e) => reject(e);
reader.readAsDataURL(blob);
})
);
}
return Promise.resolve(thumbnailUrl);
};
@customElement("ha-media-browser-thumbnail")
export class HaMediaBrowserThumbnail extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public url?: string;
@state() private _resolvedUrl?: string;
@state() private _small = false;
@state() private _brand = false;
protected willUpdate(changedProps: PropertyValues): void {
super.willUpdate(changedProps);
if (changedProps.has("url")) {
this._resolve();
}
}
private async _resolve(): Promise<void> {
this._small = false;
this._brand = !!this.url && isBrandUrl(this.url);
if (!this.url) {
this._resolvedUrl = undefined;
return;
}
const requested = this.url;
try {
const resolved = await resolveThumbnailURL(this.hass, requested);
if (requested !== this.url) return;
this._resolvedUrl = resolved;
this._probeSize(resolved);
} catch (_err) {
if (requested === this.url) this._resolvedUrl = undefined;
}
}
private _probeSize(url: string): void {
// SVGs (including brand icons) scale natively; pixelated rendering would
// break vector output.
if (this.url && isBrandUrl(this.url)) return;
if (isSvgUrl(url)) return;
const img = new Image();
img.addEventListener("load", () => {
if (this._resolvedUrl !== url) return;
if (
img.naturalWidth > 0 &&
img.naturalWidth <= SMALL_THUMBNAIL_THRESHOLD
) {
this._small = true;
}
});
img.src = url;
}
protected render(): TemplateResult | typeof nothing {
if (!this._resolvedUrl) return nothing;
return html`
<div
class=${classMap({
image: true,
small: this._small,
brand: this._brand,
})}
style="background-image: url(${this._resolvedUrl})"
></div>
`;
}
static readonly styles: CSSResultGroup = css`
:host {
display: block;
width: 100%;
height: 100%;
}
.image {
width: 100%;
height: 100%;
background-size: var(--ha-media-browser-thumbnail-fit, contain);
background-repeat: no-repeat;
background-position: center;
}
.image.brand {
background-size: 40%;
}
.image.small {
image-rendering: pixelated;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-media-browser-thumbnail": HaMediaBrowserThumbnail;
}
}
@@ -13,7 +13,6 @@ import {
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { styleMap } from "lit/directives/style-map";
import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
import { slugify } from "../../common/string/slugify";
import { debounce } from "../../common/util/debounce";
@@ -39,11 +38,6 @@ import { showAlertDialog } from "../../dialogs/generic/show-dialog-box";
import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import {
brandsUrl,
extractDomainFromBrandUrl,
isBrandUrl,
} from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
import "../entity/ha-entity-picker";
import "../ha-alert";
@@ -52,6 +46,7 @@ import "../ha-card";
import "../ha-icon-button";
import "../ha-list";
import "../ha-list-item";
import "./ha-media-browser-thumbnail";
import "../ha-spinner";
import "../ha-svg-icon";
import "../ha-tooltip";
@@ -411,12 +406,6 @@ export class HaMediaPlayerBrowse extends LitElement {
? MediaClassBrowserSettings[currentItem.children_media_class]
: MediaClassBrowserSettings.directory;
const backgroundImage = currentItem.thumbnail
? this._getThumbnailURLorBase64(currentItem.thumbnail).then(
(value) => `url(${value})`
)
: "none";
return html`
${
currentItem.can_play
@@ -431,13 +420,11 @@ export class HaMediaPlayerBrowse extends LitElement {
<div class="header-content">
${currentItem.thumbnail
? html`
<div
class="img"
style="background-image: ${until(
backgroundImage,
""
)}"
>
<div class="img">
<ha-media-browser-thumbnail
.hass=${this.hass}
.url=${currentItem.thumbnail}
></ha-media-browser-thumbnail>
${this.narrow &&
currentItem?.can_play &&
(!this.accept ||
@@ -638,12 +625,6 @@ export class HaMediaPlayerBrowse extends LitElement {
}
private _renderGridItem = (child: MediaPlayerItem): TemplateResult => {
const backgroundImage = child.thumbnail
? this._getThumbnailURLorBase64(child.thumbnail).then(
(value) => `url(${value})`
)
: "none";
return html`
<div class="child" .item=${child} @click=${this._childClicked}>
<ha-card outlined>
@@ -655,10 +636,13 @@ export class HaMediaPlayerBrowse extends LitElement {
"centered-image": ["app", "directory"].includes(
child.media_class
),
"brand-image": isBrandUrl(child.thumbnail),
})} image"
style="background-image: ${until(backgroundImage, "")}"
></div>
>
<ha-media-browser-thumbnail
.hass=${this.hass}
.url=${child.thumbnail}
></ha-media-browser-thumbnail>
</div>
`
: html`
<div class="icon-holder image">
@@ -703,13 +687,7 @@ export class HaMediaPlayerBrowse extends LitElement {
private _renderListItem = (child: MediaPlayerItem): TemplateResult => {
const currentItem = this._currentItem;
const mediaClass = MediaClassBrowserSettings[currentItem!.media_class];
const backgroundImage =
mediaClass.show_list_images && child.thumbnail
? this._getThumbnailURLorBase64(child.thumbnail).then(
(value) => `url(${value})`
)
: "none";
const showImage = mediaClass.show_list_images && !!child.thumbnail;
return html`
<ha-list-item
@@ -717,7 +695,7 @@ export class HaMediaPlayerBrowse extends LitElement {
.item=${child}
.graphic=${mediaClass.show_list_images ? "medium" : "avatar"}
>
${backgroundImage === "none" && !child.can_play
${!showImage && !child.can_play
? html`<ha-svg-icon
.path=${MediaClassBrowserSettings[
child.media_class === "directory"
@@ -731,9 +709,14 @@ export class HaMediaPlayerBrowse extends LitElement {
graphic: true,
thumbnail: mediaClass.show_list_images === true,
})}
style="background-image: ${until(backgroundImage, "")}"
slot="graphic"
>
${showImage
? html`<ha-media-browser-thumbnail
.hass=${this.hass}
.url=${child.thumbnail}
></ha-media-browser-thumbnail>`
: nothing}
${child.can_play
? html`<ha-icon-button
class="play ${classMap({
@@ -753,51 +736,6 @@ export class HaMediaPlayerBrowse extends LitElement {
`;
};
private async _getThumbnailURLorBase64(
thumbnailUrl: string | undefined
): Promise<string> {
if (!thumbnailUrl) {
return "";
}
if (isBrandUrl(thumbnailUrl)) {
// The backend is not aware of the theme used by the users,
// so we rewrite the URL to show a proper icon
return brandsUrl(
{
domain: extractDomainFromBrandUrl(thumbnailUrl),
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
);
}
if (thumbnailUrl.startsWith("/")) {
// Thumbnails served by local API require authentication
return new Promise((resolve, reject) => {
this.hass
.fetchWithAuth(thumbnailUrl!)
// Since we are fetching with an authorization header, we cannot just put the
// URL directly into the document; we need to embed the image. We could do this
// using blob URLs, but then we would need to keep track of them in order to
// release them properly. Instead, we embed the thumbnail using base64.
.then((response) => response.blob())
.then((blob) => {
const reader = new FileReader();
reader.onload = () => {
const result = reader.result;
resolve(typeof result === "string" ? result : "");
};
reader.onerror = (e) => reject(e);
reader.readAsDataURL(blob);
});
});
}
return thumbnailUrl;
}
private _actionClicked = (ev: MouseEvent): void => {
ev.stopPropagation();
const item = (ev.currentTarget as any).item;
@@ -1048,14 +986,20 @@ export class HaMediaPlayerBrowse extends LitElement {
align-items: flex-start;
}
.header-content .img {
position: relative;
height: 175px;
width: 175px;
margin-right: 16px;
background-size: cover;
border-radius: 2px;
overflow: hidden;
transition:
width 0.4s,
height 0.4s;
--ha-media-browser-thumbnail-fit: cover;
}
.header-content .img ha-media-browser-thumbnail {
position: absolute;
inset: 0;
}
.header-info {
display: flex;
@@ -1191,18 +1135,12 @@ export class HaMediaPlayerBrowse extends LitElement {
right: 0;
left: 0;
bottom: 0;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
--ha-media-browser-thumbnail-fit: cover;
}
.centered-image {
margin: 0 8px;
background-size: contain;
}
.brand-image {
background-size: 40%;
--ha-media-browser-thumbnail-fit: contain;
}
.children ha-card .icon-holder {
@@ -1278,17 +1216,21 @@ export class HaMediaPlayerBrowse extends LitElement {
}
ha-list-item .graphic {
background-size: contain;
background-repeat: no-repeat;
background-position: center;
position: relative;
border-radius: var(--ha-border-radius-sm);
display: flex;
align-content: center;
align-items: center;
overflow: hidden;
line-height: initial;
}
ha-list-item .graphic ha-media-browser-thumbnail {
position: absolute;
inset: 0;
}
ha-list-item .graphic .play {
position: absolute;
inset: 0;
margin: auto;
opacity: 0;
transition: all 0.5s;
background-color: rgba(var(--rgb-card-background-color), 0.5);
+2
View File
@@ -99,6 +99,8 @@ export class HaRadioOption extends Radio {
--ha-radio-option-checked-background-color,
var(--ha-color-fill-primary-normal-resting)
);
color: var(--checked-icon-color);
border-color: var(--checked-icon-color);
}
[part~="label"] {
+17 -11
View File
@@ -1,4 +1,3 @@
import { createContext } from "@lit/context";
import type {
Connection,
HassEntityAttributeBase,
@@ -96,7 +95,7 @@ export interface TriggerList {
export interface BaseTrigger {
alias?: string;
comment?: string;
note?: string;
/** @deprecated Use `trigger` instead */
platform?: string;
trigger: string;
@@ -242,7 +241,7 @@ export type Trigger = LegacyTrigger | TriggerList | PlatformTrigger;
interface BaseCondition {
condition: string;
alias?: string;
comment?: string;
note?: string;
enabled?: boolean;
options?: Record<string, unknown>;
}
@@ -486,17 +485,28 @@ export const migrateAutomationTrigger = (
}
delete trigger.platform;
}
if ("options" in trigger) {
if (trigger.options && "behavior" in trigger.options) {
if (trigger.options.behavior === "any") {
trigger.options.behavior = "each";
} else if (trigger.options.behavior === "last") {
trigger.options.behavior = "all";
}
}
}
return trigger;
};
export const flattenTriggers = (
triggers: undefined | Trigger | Trigger[]
): Exclude<Trigger, TriggerList>[] => {
): Trigger[] => {
if (!triggers) {
return [];
}
const flatTriggers: Exclude<Trigger, TriggerList>[] = [];
const flatTriggers: Trigger[] = [];
ensureArray(triggers).forEach((t) => {
if ("triggers" in t) {
@@ -610,7 +620,7 @@ export interface AutomationClipboard {
export interface BaseSidebarConfig {
delete: () => void;
close: (focus?: boolean) => void;
editComment: () => void;
editNote: () => void;
}
export interface TriggerSidebarConfig extends BaseSidebarConfig {
@@ -672,7 +682,7 @@ export interface OptionSidebarConfig extends BaseSidebarConfig {
rename: () => void;
duplicate: () => void;
defaultOption?: boolean;
comment?: string;
note?: string;
}
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
@@ -698,7 +708,3 @@ export interface ShowAutomationEditorParams {
data?: Partial<AutomationConfig>;
expanded?: boolean;
}
export const automationConfigContext = createContext<
AutomationConfig | undefined
>("automationConfig");
-36
View File
@@ -27,7 +27,6 @@ import type {
LegacyTrigger,
Trigger,
} from "./automation";
import { flattenTriggers } from "./automation";
import { getConditionDomain, getConditionObjectId } from "./condition";
import type {
DeviceCondition,
@@ -108,41 +107,6 @@ const formatNumericLimitValue = (
: value;
};
export interface TriggerInfo {
id: string;
label: string;
triggerType: string;
count: number;
}
export const getTriggerInfos = (
triggers: Trigger[] | undefined,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[]
): TriggerInfo[] => {
if (!triggers) {
return [];
}
const map = new Map<string, TriggerInfo>();
for (const t of flattenTriggers(triggers)) {
if (isTriggerList(t) || !t.id) {
continue;
}
const existing = map.get(t.id);
if (existing) {
existing.count++;
} else {
map.set(t.id, {
id: t.id,
label: describeTrigger(t, hass, entityRegistry),
triggerType: t.trigger,
count: 1,
});
}
}
return Array.from(map.values());
};
export const describeTrigger = (
trigger: Trigger,
hass: HomeAssistant,
+1
View File
@@ -256,6 +256,7 @@ export const normalizeSubscriptionEventData = (
dtstart: eventStart,
dtend: eventEnd,
description: eventData.description ?? undefined,
location: eventData.location ?? undefined,
uid: eventData.uid ?? undefined,
recurrence_id: eventData.recurrence_id ?? undefined,
rrule: eventData.rrule ?? undefined,
-1
View File
@@ -40,7 +40,6 @@ export const createConfigFlow = (
"config/config_entries/flow",
{
handler,
show_advanced_options: Boolean(hass.userData?.showAdvanced),
entry_id,
},
HEADERS
+1 -1
View File
@@ -13,7 +13,7 @@ import {
export interface DeviceAutomation {
alias?: string;
comment?: string;
note?: string;
device_id: string;
domain: string;
entity_id?: string;
+7 -1
View File
@@ -161,6 +161,10 @@ export interface VacuumEntityOptions {
last_seen_segments?: Segment[];
}
export interface DeviceTrackerEntityOptions {
associated_zone?: string | null;
}
export interface EntityRegistryOptions {
number?: NumberEntityOptions;
sensor?: SensorEntityOptions;
@@ -172,6 +176,7 @@ export interface EntityRegistryOptions {
cover?: CoverEntityOptions;
valve?: ValveEntityOptions;
vacuum?: VacuumEntityOptions;
device_tracker?: DeviceTrackerEntityOptions;
switch_as_x?: SwitchAsXEntityOptions;
conversation?: Record<string, unknown>;
"cloud.alexa"?: Record<string, unknown>;
@@ -197,7 +202,8 @@ export interface EntityRegistryEntryUpdateParams {
| LightEntityOptions
| CoverEntityOptions
| ValveEntityOptions
| VacuumEntityOptions;
| VacuumEntityOptions
| DeviceTrackerEntityOptions;
aliases?: (string | null)[];
labels?: string[];
categories?: Record<string, string | null>;
-1
View File
@@ -2,7 +2,6 @@ import type { Connection } from "home-assistant-js-websocket";
import type { ShortcutItem } from "./home_shortcuts";
export interface CoreFrontendUserData {
showAdvanced?: boolean;
showEntityIdPicker?: boolean;
default_panel?: string;
apps_info_dismissed?: boolean;
+12
View File
@@ -1,6 +1,14 @@
import type { HassEntity } from "home-assistant-js-websocket";
import type { HomeAssistant } from "../types";
import type { LovelaceCardFeatureContext } from "../panels/lovelace/card-features/types";
import type { LovelaceCardConfig } from "./lovelace/config/card";
export interface CustomCardSuggestion<
T extends LovelaceCardConfig = LovelaceCardConfig,
> {
label?: string;
config: T;
}
export interface CustomCardEntry {
type: string;
@@ -8,6 +16,10 @@ export interface CustomCardEntry {
description?: string;
preview?: boolean;
documentationURL?: string;
getEntitySuggestion?: (
hass: HomeAssistant,
entityId: string
) => CustomCardSuggestion | CustomCardSuggestion[] | null;
}
export interface CustomBadgeEntry {
+16 -5
View File
@@ -2,7 +2,10 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { navigate } from "../common/navigate";
import type { HomeAssistant } from "../types";
import { subscribeDeviceRegistry } from "./device/device_registry";
import {
subscribeDeviceRegistry,
type DeviceRegistryEntry,
} from "./device/device_registry";
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
export enum NetworkType {
@@ -77,9 +80,9 @@ export const startExternalCommissioning = async (hass: HomeAssistant) => {
});
};
export const redirectOnNewMatterDevice = (
export const watchForNewMatterDevice = (
hass: HomeAssistant,
callback?: () => void
callback: (device: DeviceRegistryEntry) => void
): UnsubscribeFunc => {
let curMatterDevices: Set<string> | undefined;
const unsubDeviceReg = subscribeDeviceRegistry(hass.connection, (entries) => {
@@ -101,8 +104,7 @@ export const redirectOnNewMatterDevice = (
if (newMatterDevices.length) {
unsubDeviceReg();
curMatterDevices = undefined;
callback?.();
navigate(`/config/devices/device/${newMatterDevices[0].id}`);
callback(newMatterDevices[0]);
}
});
return () => {
@@ -111,6 +113,15 @@ export const redirectOnNewMatterDevice = (
};
};
export const redirectOnNewMatterDevice = (
hass: HomeAssistant,
callback?: () => void
): UnsubscribeFunc =>
watchForNewMatterDevice(hass, (device) => {
callback?.();
navigate(`/config/devices/device/${device.id}`);
});
export const addMatterDevice = (hass: HomeAssistant) => {
startExternalCommissioning(hass);
};
-1
View File
@@ -7,7 +7,6 @@ export const createOptionsFlow = (hass: HomeAssistant, handler: string) =>
"config/config_entries/options/flow",
{
handler,
show_advanced_options: Boolean(hass.userData?.showAdvanced),
}
);
+3 -3
View File
@@ -36,7 +36,7 @@ export const isMaxMode = arrayLiteralIncludes(MODES_MAX);
export const baseActionStruct = object({
alias: optional(string()),
comment: optional(string()),
note: optional(string()),
continue_on_error: optional(boolean()),
enabled: optional(boolean()),
});
@@ -106,7 +106,7 @@ export interface Field {
interface BaseAction {
alias?: string;
comment?: string;
note?: string;
continue_on_error?: boolean;
enabled?: boolean;
}
@@ -197,7 +197,7 @@ export interface ForEachRepeat extends BaseRepeat {
export interface Option {
alias?: string;
comment?: string;
note?: string;
conditions: string | Condition[];
sequence: Action | Action[];
}
+1 -1
View File
@@ -125,7 +125,7 @@ export interface BooleanSelector {
boolean: {} | null;
}
export type AutomationBehaviorTriggerMode = "first" | "last" | "any";
export type AutomationBehaviorTriggerMode = "first" | "all" | "each";
export type AutomationBehaviorConditionMode = "all" | "any";
-1
View File
@@ -16,7 +16,6 @@ export const createSubConfigFlow = (
"config/config_entries/subentries/flow",
{
handler: [configEntryId, subFlowType],
show_advanced_options: Boolean(hass.userData?.showAdvanced),
subentry_id,
},
HEADERS
-44
View File
@@ -8,7 +8,6 @@ import type {
Trigger,
TriggerList,
} from "./automation";
import { flattenTriggers } from "./automation";
import type { Selector, TargetSelector } from "./selector";
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
@@ -57,49 +56,6 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger;
export const getTriggerIds = (triggers: Trigger[]): string[] =>
flattenTriggers(triggers)
.map((trigger) => trigger.id)
.filter((id): id is string => !!id);
export const getNextNumericTriggerId = (triggers: Trigger[]): string => {
let max = 0;
for (const id of getTriggerIds(triggers)) {
const num = Number(id);
if (Number.isInteger(num) && num > max) {
max = num;
}
}
return String(max + 1);
};
const computeUniqueId = (id: string, existing: Set<string>): string => {
if (!existing.has(id)) {
return id;
}
// Split into a base and a trailing integer suffix so we can bump the
// suffix on collision (e.g. "foo2" -> "foo3"); if there's no trailing
// digit we start at 2 ("foo" -> "foo2").
const match = id.match(/^(.*?)(\d+)$/);
let base: string;
let num: number;
if (match) {
base = match[1];
num = Number(match[2]) + 1;
} else {
base = id;
num = 2;
}
while (existing.has(`${base}${num}`)) {
num++;
}
return `${base}${num}`;
};
export const getUniqueTriggerId = (id: string, triggers: Trigger[]): string =>
computeUniqueId(id, new Set(getTriggerIds(triggers)));
export interface TriggerDescription {
target?: TargetSelector["target"];
fields: Record<
@@ -1,26 +1,37 @@
import { computeDomain } from "../../common/entity/compute_domain";
import { navigate } from "../../common/navigate";
import type { LocalizeFunc } from "../../common/translations/localize";
import type { LocalizeKeys } from "../../common/translations/localize";
import { createSearchParam } from "../../common/url/search-params";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import { SCENE_IGNORED_DOMAINS, type SceneEntities } from "../../data/scene";
import type { SingleHassServiceTarget } from "../../data/target";
import {
ADD_AUTOMATION_ELEMENT_AREA_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
} from "../../panels/config/automation/show-add-automation-element-dialog";
import type { HomeAssistant, TranslationDict } from "../../types";
/** Add to action keys are the keys of the translation dictionary for the add to actions. */
export type AddToActionKey =
TranslationDict["ui"]["dialogs"]["more_info_control"]["add_to"]["actions"] extends infer Actions
? keyof Actions
: never;
/** Add to action keys are the keys of the translation dictionary for the add to action options. */
type AddToActionOptions =
TranslationDict["ui"]["dialogs"]["more_info_control"]["add_to"]["action_options"];
export type AddToActionKey = Extract<keyof AddToActionOptions, string>;
export type AddToAutomationScriptActionKey = Exclude<AddToActionKey, "scene">;
/** Fully-qualified localize key for an add to action option label. */
type AddToActionOptionLabelKey = LocalizeKeys &
`ui.dialogs.more_info_control.add_to.action_options.${AddToActionKey}`;
interface BaseEntityAddToAction {
/** Whether the action is enabled and can be selected. */
enabled: boolean;
/** Translated name of the action */
name: string;
/** Translated label of the action option */
name?: string;
/** Fully-qualified localize key for the action option label */
nameKey?: AddToActionOptionLabelKey;
/** Optional translated description of the action */
description?: string;
/** MDI icon name (e.g., "mdi:car") */
@@ -31,7 +42,7 @@ export interface DefaultEntityAddToAction extends BaseEntityAddToAction {
/** Type of action handled in the frontend */
type: "default";
/** Stable key used to resolve the action handler */
key: AddToActionKey;
key: AddToAutomationScriptActionKey;
}
export interface ExternalEntityAddToAction extends BaseEntityAddToAction {
@@ -48,11 +59,11 @@ export type EntityAddToAction =
export type EntityAddToActions = EntityAddToAction[];
interface ActionDefinition {
translation_key: AddToActionKey;
translation_key: AddToAutomationScriptActionKey;
icon: string;
}
export const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
{
translation_key: "automation_trigger",
icon: "mdi:robot-outline",
@@ -71,33 +82,49 @@ export const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
},
];
export const getDefaultAddToActions = (
states: HomeAssistant["states"],
localize: LocalizeFunc,
formatEntityName: HomeAssistant["formatEntityName"],
entityId: string
): EntityAddToActions =>
export const getDefaultAddToActions = (): EntityAddToActions =>
DEFAULT_ACTION_DEFS.map(
(def: ActionDefinition): EntityAddToAction => ({
type: "default",
key: def.translation_key,
enabled: true,
name: localize(
`ui.dialogs.more_info_control.add_to.actions.${def.translation_key}`,
{
target:
states[entityId] !== undefined
? formatEntityName(states[entityId], undefined)
: entityId,
}
),
nameKey: `ui.dialogs.more_info_control.add_to.action_options.${def.translation_key}`,
icon: def.icon,
})
);
export const createAddToSceneEntities = (
entityIds: string[]
): SceneEntities => {
const entities: SceneEntities = {};
for (const entityId of entityIds) {
entities[entityId] = "";
}
return entities;
};
export const filterAddToSceneEntityIds = (
entityIds: string[],
entityRegistry: readonly EntityRegistryEntry[],
states: HomeAssistant["states"]
): string[] => {
const entityIdSet = new Set(entityIds);
return entityRegistry
.filter((entry) => entityIdSet.has(entry.entity_id))
.filter(
(entry) =>
!entry.entity_category &&
!entry.hidden_by &&
!SCENE_IGNORED_DOMAINS.includes(computeDomain(entry.entity_id)) &&
states[entry.entity_id]
)
.map((entry) => entry.entity_id);
};
/** Handler for adding a target to an automation/script. */
export function addToActionHandler(
key: AddToActionKey,
key: AddToAutomationScriptActionKey,
target: SingleHassServiceTarget
): Promise<boolean> {
const searchParams: Record<string, string> = {};
+211
View File
@@ -0,0 +1,211 @@
import { mdiPlus } from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import type {
HASSDomCurrentTargetEvent,
HASSDomEvent,
} from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import type {
LocalizeFunc,
LocalizeKeys,
} from "../../common/translations/localize";
import "../../components/ha-icon";
import "../../components/ha-svg-icon";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
export interface AddToActionListItem {
name?: string;
nameKey?: LocalizeKeys;
description?: string;
descriptionKey?: LocalizeKeys;
icon?: string;
iconPath?: string;
enabled?: boolean;
}
export interface AddToActionListSection<
Item extends AddToActionListItem = AddToActionListItem,
> {
title?: string;
titleKey?: LocalizeKeys;
actions: readonly Item[];
empty?: string;
emptyKey?: LocalizeKeys;
}
export interface AddToActionListActionSelectedDetail<
Item extends AddToActionListItem = AddToActionListItem,
> {
action: Item;
}
export type AddToActionListActionSelectedEvent<
Item extends AddToActionListItem = AddToActionListItem,
> = HASSDomEvent<AddToActionListActionSelectedDetail<Item>>;
@customElement("ha-add-to-action-list")
class HaAddToActionList extends LitElement {
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false })
public sections: readonly AddToActionListSection[] = [];
protected render(): TemplateResult | typeof nothing {
if (!this.sections.length) {
return nothing;
}
return html`${this.sections.map((section, sectionIndex) =>
this._renderSection(section, sectionIndex)
)}`;
}
private _renderSection(
section: AddToActionListSection,
sectionIndex: number
): TemplateResult | typeof nothing {
if (!section.actions.length && !section.empty && !section.emptyKey) {
return nothing;
}
return html`
<h3 class="section-header">
${this._localizeValue(section.title, section.titleKey)}
</h3>
${section.actions.length
? html`<ha-list-base>
${section.actions.map((action, actionIndex) =>
this._renderActionItem(action, sectionIndex, actionIndex)
)}
</ha-list-base>`
: html`<h4 class="empty">
${this._localizeValue(section.empty, section.emptyKey)}
</h4>`}
`;
}
private _renderActionItem(
action: AddToActionListItem,
sectionIndex: number,
actionIndex: number
): TemplateResult {
return html`
<ha-list-item-button
.disabled=${action.enabled === false}
data-section-index=${sectionIndex}
data-action-index=${actionIndex}
.headline=${this._localizeValue(action.name, action.nameKey)}
.supportingText=${this._localizeValue(
action.description,
action.descriptionKey
)}
@click=${this._actionSelected}
>
${action.icon
? html`<ha-icon
class="start-icon"
slot="start"
.icon=${action.icon}
></ha-icon>`
: action.iconPath
? html`<ha-svg-icon
class="start-icon"
slot="start"
.path=${action.iconPath}
></ha-svg-icon>`
: nothing}
<ha-svg-icon class="plus" slot="end" .path=${mdiPlus}></ha-svg-icon>
</ha-list-item-button>
`;
}
private _localizeValue(
value?: string,
localizeKey?: LocalizeKeys
): string | undefined {
return value || (localizeKey ? this._localize(localizeKey) : undefined);
}
private _actionSelected(
ev: HASSDomCurrentTargetEvent<HaListItemButton>
): void {
const action =
this.sections[Number(ev.currentTarget.dataset.sectionIndex)]?.actions[
Number(ev.currentTarget.dataset.actionIndex)
];
if (!action) {
return;
}
if (action.enabled === false) {
return;
}
fireEvent(this, "add-to-list-action-selected", {
action,
});
}
static styles: CSSResultGroup = css`
:host {
display: block;
}
.section-header {
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
margin: 0;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
color: var(--secondary-text-color);
}
.empty {
padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-1);
margin: 0;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-normal);
color: var(--secondary-text-color);
}
ha-list-item-button {
--ha-row-item-padding-inline: var(--ha-space-5);
}
ha-icon,
ha-svg-icon {
display: flex;
align-items: center;
}
.start-icon {
color: var(--ha-color-text-secondary);
}
.plus {
color: var(--primary-color);
}
ha-list-item-button[disabled] .start-icon,
ha-list-item-button[disabled] .plus {
color: var(--disabled-text-color);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-add-to-action-list": HaAddToActionList;
}
interface HASSDomEvents {
"add-to-list-action-selected": AddToActionListActionSelectedDetail;
}
}
@@ -167,7 +167,6 @@ export interface DataEntryFlowDialogParams {
entryId?: string;
}) => void;
flowConfig: FlowConfig;
showAdvanced?: boolean;
dialogParentElement?: HTMLElement;
navigateToResult?: boolean;
carryOverDevices?: string[];
@@ -48,7 +48,6 @@ class StepFlowAbort extends LitElement {
showConfigFlowDialog(this.params.dialogParentElement!, {
dialogClosedCallback: this.params.dialogClosedCallback,
startFlowHandler: this.handler,
showAdvanced: this.hass.userData?.showAdvanced,
navigateToResult: this.params.navigateToResult,
});
},
+55 -75
View File
@@ -1,26 +1,35 @@
import { LitElement, css, html, nothing } from "lit";
import { consume, type ContextType } from "@lit/context";
import { LitElement, css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert";
import "../../components/ha-icon";
import "../../components/ha-spinner";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import { showToast } from "../../util/toast";
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import { configContext } from "../../data/context";
import "../add-to/ha-add-to-action-list";
import type {
AddToActionListActionSelectedEvent,
AddToActionListSection,
} from "../add-to/ha-add-to-action-list";
import {
type EntityAddToAction,
type EntityAddToActions,
addToActionHandler,
getDefaultAddToActions,
} from "./add-to";
} from "../add-to/add-to";
import { consumeLocalize } from "../../common/decorators/consume-context-entry";
import type { LocalizeFunc } from "../../common/translations/localize";
@customElement("ha-more-info-add-to")
export class HaMoreInfoAddTo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state()
@consume({ context: configContext, subscribe: true })
private _config?: ContextType<typeof configContext>;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@property({ attribute: false }) public entityId!: string;
@@ -31,18 +40,13 @@ export class HaMoreInfoAddTo extends LitElement {
@state() private _loading = true;
private async _loadActions() {
this._defaultActions = getDefaultAddToActions(
this.hass.states,
this.hass.localize,
this.hass.formatEntityName,
this.entityId
);
this._defaultActions = getDefaultAddToActions();
this._externalActions = [];
if (this.hass.auth.external?.config.hasEntityAddTo) {
if (this._config?.auth.external?.config.hasEntityAddTo) {
try {
const response =
await this.hass.auth.external?.sendMessage<"entity/add_to/get_actions">(
await this._config.auth.external.sendMessage<"entity/add_to/get_actions">(
{
type: "entity/add_to/get_actions",
payload: { entity_id: this.entityId },
@@ -66,13 +70,9 @@ export class HaMoreInfoAddTo extends LitElement {
}
private async _actionSelected(
ev: HASSDomCurrentTargetEvent<
HaListItemButton & {
action: EntityAddToAction;
}
>
ev: AddToActionListActionSelectedEvent<EntityAddToAction>
) {
const action = ev.currentTarget.action;
const { action } = ev.detail;
if (!action.enabled) {
return;
}
@@ -82,7 +82,10 @@ export class HaMoreInfoAddTo extends LitElement {
if (!action.payload) {
throw new Error("Missing external action payload");
}
this.hass.auth.external!.fireMessage({
if (!this._config?.auth.external) {
throw new Error("Missing external app connection");
}
this._config.auth.external.fireMessage({
type: "entity/add_to",
payload: {
entity_id: this.entityId,
@@ -92,7 +95,7 @@ export class HaMoreInfoAddTo extends LitElement {
fireEvent(this, "add-to-action-selected");
} catch (err: unknown) {
showToast(this, {
message: this.hass.localize(
message: this._localize(
"ui.dialogs.more_info_control.add_to.action_failed",
{
error: err instanceof Error ? err.message : String(err),
@@ -110,24 +113,6 @@ export class HaMoreInfoAddTo extends LitElement {
addToActionHandler(action.key, { entity_id: this.entityId });
}
private _renderActionItems(actions: EntityAddToActions) {
return actions.map(
(action) => html`
<ha-list-item-button
.disabled=${!action.enabled}
.action=${action}
@click=${this._actionSelected}
>
<ha-icon slot="start" .icon=${action.icon}></ha-icon>
<span slot="headline">${action.name}</span>
${action.description
? html`<span slot="supporting-text">${action.description}</span>`
: nothing}
</ha-list-item-button>
`
);
}
protected async firstUpdated() {
await this._loadActions();
this._loading = false;
@@ -145,29 +130,38 @@ export class HaMoreInfoAddTo extends LitElement {
if (!this._defaultActions.length && !this._externalActions.length) {
return html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.no_actions"
)}
${this._localize("ui.dialogs.more_info_control.add_to.no_actions")}
</ha-alert>
`;
}
const automationActions = this._defaultActions.filter(
(action) => action.type === "default" && action.key !== "script_action"
);
const scriptActions = this._defaultActions.filter(
(action) => action.type === "default" && action.key === "script_action"
);
const sections: AddToActionListSection<EntityAddToAction>[] = [
{
titleKey: "ui.dialogs.more_info_control.add_to.automations_heading",
actions: automationActions,
},
{
titleKey: "ui.dialogs.more_info_control.add_to.scripts_heading",
actions: scriptActions,
},
{
titleKey: "ui.dialogs.more_info_control.add_to.app_actions",
actions: this._externalActions,
},
];
return html`
<ha-list-base>
${this._renderActionItems(this._defaultActions)}
</ha-list-base>
${this._externalActions.length
? html`
<h2 class="section-title">
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.app_actions"
)}
</h2>
<ha-list-base>
${this._renderActionItems(this._externalActions)}
</ha-list-base>
`
: nothing}
<ha-add-to-action-list
.sections=${sections}
@add-to-list-action-selected=${this._actionSelected}
></ha-add-to-action-list>
`;
}
@@ -183,20 +177,6 @@ export class HaMoreInfoAddTo extends LitElement {
align-items: center;
padding: var(--ha-space-8);
}
.section-title {
padding: 0 var(--ha-space-6);
margin: var(--ha-space-4) 0 var(--ha-space-1);
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
line-height: var(--ha-line-height-normal);
color: var(--secondary-text-color);
}
ha-icon {
display: flex;
align-items: center;
}
`;
}
+14 -9
View File
@@ -23,6 +23,7 @@ import { customElement, property, query, state } from "lit/decorators";
import { cache } from "lit/directives/cache";
import { classMap } from "lit/directives/class-map";
import { keyed } from "lit/directives/keyed";
import type { RequestSelectedDetail } from "@material/mwc-list/mwc-list-item";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import type { HASSDomEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
@@ -517,7 +518,7 @@ export class MoreInfoDialog extends SubscribeMixin(
await favoritesHandler.copy(favoritesContext);
}
private _goToAddEntityTo(ev) {
private _goToAddEntityTo(ev: CustomEvent<RequestSelectedDetail>) {
// Only check for request-selected events (from menu items), not regular clicks (from icon button)
if (
ev.type === "request-selected" &&
@@ -590,10 +591,19 @@ export class MoreInfoDialog extends SubscribeMixin(
(v): v is string => Boolean(v)
);
const defaultTitle = breadcrumb.pop() || entityId;
const addToTitle = this.hass.localize(
"ui.dialogs.more_info_control.add_to.title",
{ target: defaultTitle }
);
const addToMenuItem = this.hass.localize(
"ui.dialogs.more_info_control.add_to.item"
);
const title =
this._currView === "details"
? this.hass.localize("ui.dialogs.more_info_control.details")
: this._childView?.viewTitle || defaultTitle;
: this._currView === "add_to"
? addToTitle
: this._childView?.viewTitle || defaultTitle;
const favoritesContext =
this._entry && stateObj
@@ -711,9 +721,7 @@ export class MoreInfoDialog extends SubscribeMixin(
slot="icon"
.path=${mdiPlusBoxMultipleOutline}
></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.title"
)}
${addToMenuItem}
</ha-dropdown-item>
<wa-divider></wa-divider>
@@ -814,9 +822,7 @@ export class MoreInfoDialog extends SubscribeMixin(
? html`
<ha-icon-button
slot="headerActionItems"
.label=${this.hass.localize(
"ui.dialogs.more_info_control.add_to.title"
)}
.label=${addToMenuItem}
.path=${mdiPlusBoxMultipleOutline}
@click=${this._goToAddEntityTo}
></ha-icon-button>
@@ -906,7 +912,6 @@ export class MoreInfoDialog extends SubscribeMixin(
: this._currView === "add_to"
? html`
<ha-more-info-add-to
.hass=${this.hass}
.entityId=${entityId}
@add-to-action-selected=${this._goBack}
></ha-more-info-add-to>
+7 -7
View File
@@ -1,8 +1,8 @@
import type { LitElement } from "lit";
import { state } from "lit/decorators";
import { listenMediaQuery } from "../common/dom/media_query";
import type { Constructor } from "../types";
import { isMobileClient } from "../util/is_mobile";
import { listenMediaQuery } from "../common/dom/media_query";
export const MobileAwareMixin = <T extends Constructor<LitElement>>(
superClass: T
@@ -12,16 +12,16 @@ export const MobileAwareMixin = <T extends Constructor<LitElement>>(
protected _isMobileClient = isMobileClient;
protected mobileSizeQuery =
"all and (max-width: 450px), all and (max-height: 500px)";
private _unsubMql?: () => void;
public connectedCallback() {
super.connectedCallback();
this._unsubMql = listenMediaQuery(
"all and (max-width: 450px), all and (max-height: 500px)",
(matches) => {
this._isMobileSize = matches;
}
);
this._unsubMql = listenMediaQuery(this.mobileSizeQuery, (matches) => {
this._isMobileSize = matches;
});
}
public disconnectedCallback() {
-3
View File
@@ -6,7 +6,6 @@ import { classMap } from "lit/directives/class-map";
import { createRef, ref } from "lit/directives/ref";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { IFRAME_SANDBOX } from "../../util/iframe";
import { navigate } from "../../common/navigate";
import { computeRouteTail } from "../../common/url/route";
import { nextRender } from "../../common/util/render-status";
@@ -137,8 +136,6 @@ class HaPanelApp extends LitElement {
})}
title=${this._addon.name}
src=${this._addon.ingress_url!}
.sandbox=${IFRAME_SANDBOX}
allow="microphone; camera; clipboard-write"
@load=${this._checkLoaded}
${ref(this._iframeRef)}
>
-1
View File
@@ -342,7 +342,6 @@ class PanelCalendar extends SubscribeMixin(LitElement) {
private _addCalendar = async (): Promise<void> => {
showConfigFlowDialog(this, {
startFlowHandler: "local_calendar",
showAdvanced: this.hass.userData?.showAdvanced,
manifest: await fetchIntegrationManifest(this.hass, "local_calendar"),
dialogClosedCallback: ({ flowFinished }) => {
if (flowFinished) {
@@ -31,7 +31,6 @@ class SupervisorAppInfoDashboard extends LitElement {
<supervisor-app-info
.narrow=${this.narrow}
.route=${this.route}
.hass=${this.hass}
.addon=${this.addon}
.controlEnabled=${this.controlEnabled}
></supervisor-app-info>
@@ -92,14 +92,15 @@ import {
showConfirmationDialog,
} from "../../../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../../../dialogs/more-info/show-ha-more-info-dialog";
import { MobileAwareMixin } from "../../../../../mixins/mobile-aware-mixin";
import { mdiHomeAssistant } from "../../../../../resources/home-assistant-logo-svg";
import { haStyle } from "../../../../../resources/styles";
import type { Route } from "../../../../../types";
import { bytesToString } from "../../../../../util/bytes-to-string";
import { getAppDisplayName } from "../../common/app";
import "../components/supervisor-app-metric";
import "../../components/supervisor-apps-tag";
import "../../components/supervisor-apps-state";
import "../../components/supervisor-apps-tag";
import "../components/supervisor-app-metric";
import { extractChangelog } from "../util/supervisor-app";
import "./supervisor-app-system-managed";
@@ -123,7 +124,7 @@ const RATING_ICON = {
const POLL_INTERVAL_SECONDS = 5;
@customElement("supervisor-app-info")
class SupervisorAppInfo extends LitElement {
class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
@property({ type: Boolean }) public narrow = false;
@property({ attribute: false }) public route!: Route;
@@ -163,6 +164,9 @@ class SupervisorAppInfo extends LitElement {
private _pollInterval?: number;
protected mobileSizeQuery =
"all and (max-width: 1120px), all and (max-height: 500px)";
private get _currentAddon(): HassioAddonDetails | StoreAddonDetails {
return this._addon || this.addon;
}
@@ -181,23 +185,25 @@ class SupervisorAppInfo extends LitElement {
private _renderInfoCard() {
const systemManaged = this._isSystemManaged(this._currentAddon);
return html`<ha-card outlined>
return html` <ha-card outlined>
<div class="card-content">
<div class="addon-header">
${this._currentAddon.logo
? html`
<img
class="logo"
alt=""
src="/api/hassio/addons/${this._currentAddon.slug}/logo"
/>
`
: nothing}
<div class="title">
${getAppDisplayName(
this._currentAddon.name,
this._currentAddon.stage
)}
${this._currentAddon.logo
? html`
<img
class="logo"
alt=""
src="/api/hassio/addons/${this._currentAddon.slug}/logo"
/>
`
: nothing}
${!this.narrow
? getAppDisplayName(
this._currentAddon.name,
this._currentAddon.stage
)
: nothing}
<div class="description">
${this._currentAddon.version
? html`
@@ -235,17 +241,7 @@ class SupervisorAppInfo extends LitElement {
? html`<supervisor-apps-state
.state=${this._currentAddon.state}
></supervisor-apps-state>`
: html`
<ha-progress-button
.disabled=${!this._currentAddon.available}
@click=${this._installClicked}
.iconPath=${mdiApplicationImport}
>
${this.i18n.localize(
"ui.panel.config.apps.dashboard.install"
)}
</ha-progress-button>
`}
: nothing}
</div>
<ha-chip-set class="capabilities">
@@ -509,7 +505,8 @@ class SupervisorAppInfo extends LitElement {
</div>
${(this._currentAddon.update_available && this._updateEntityId) ||
this._computeShowWebUI ||
this._computeShowIngressUI
this._computeShowIngressUI ||
!this._currentAddon.version
? html`
<div class="card-actions">
${this._currentAddon.update_available && this._updateEntityId
@@ -545,6 +542,19 @@ class SupervisorAppInfo extends LitElement {
</ha-button>
`
: nothing}
${!this._currentAddon.version
? html`
<ha-progress-button
.disabled=${!this._currentAddon.available}
@click=${this._installClicked}
.iconPath=${mdiApplicationImport}
>
${this.i18n.localize(
"ui.panel.config.apps.dashboard.install"
)}
</ha-progress-button>
`
: nothing}
</div>
`
: nothing}
@@ -863,11 +873,11 @@ class SupervisorAppInfo extends LitElement {
`
: nothing}
<div
class="app ${this.narrow || !this._currentAddon.version
class="app ${this._isMobileSize || !this._currentAddon.version
? "column"
: ""}"
>
${this.narrow || !this._currentAddon.version
${this._isMobileSize || !this._currentAddon.version
? html`
${this._renderInfoCard()}
${this._currentAddon.version
@@ -1493,16 +1503,17 @@ class SupervisorAppInfo extends LitElement {
}
.addon-header {
display: flex;
padding-inline-start: var(--ha-space-2);
padding-inline-end: initial;
font-size: var(--ha-font-size-2xl);
color: var(--ha-card-header-color, var(--primary-text-color));
align-items: center;
gap: var(--ha-space-2);
flex-wrap: wrap;
margin-bottom: var(--ha-space-4);
}
.addon-header .title {
flex: 1;
margin-inline-end: var(--ha-space-4);
}
.addon-header .title .description {
@@ -1521,17 +1532,15 @@ class SupervisorAppInfo extends LitElement {
color: var(--error-color);
margin-bottom: var(--ha-space-4);
}
.description {
margin-bottom: var(--ha-space-4);
}
.description a {
color: var(--primary-color);
}
img.logo {
max-width: 100%;
max-height: 60px;
max-height: 40px;
display: block;
margin-bottom: var(--ha-space-2);
}
ha-assist-chip {
--md-sys-color-primary: var(--text-primary-color);
@@ -1,5 +1,5 @@
import "@home-assistant/webawesome/dist/components/tag/tag";
import { mdiHelpCircleOutline } from "@mdi/js";
import { mdiCheckCircle, mdiHelpCircleOutline } from "@mdi/js";
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
@@ -25,7 +25,9 @@ class SupervisorAppsCardContent extends LitElement {
@property() public stage: AddonStage = "stable";
@property() public state: AddonState = null;
@property() public state?: AddonState;
@property({ type: Boolean }) public installed = false;
@property() public description?: string;
@@ -77,13 +79,23 @@ class SupervisorAppsCardContent extends LitElement {
</div>
</div>
</div>
${this.tags?.length || this.state
${this.tags?.length || this.state !== undefined || this.installed
? html`
<div class="footer">
<supervisor-apps-state
.state=${this.state || "unknown"}
></supervisor-apps-state>
${this.state !== undefined
? html`<supervisor-apps-state
.state=${this.state || "unknown"}
></supervisor-apps-state>`
: this.installed
? html`<div class="installed">
<ha-svg-icon .path=${mdiCheckCircle}></ha-svg-icon>
<span
>${this.hass.localize(
"ui.panel.config.apps.state.installed"
)}</span
>
</div>`
: html`<span></span>`}
${this.tags?.length
? html`<div class="tags">
${this.tags.map(
@@ -159,6 +171,17 @@ class SupervisorAppsCardContent extends LitElement {
display: flex;
gap: var(--ha-space-2);
}
.installed {
display: inline-flex;
align-items: center;
gap: var(--ha-space-2);
color: var(--ha-color-text-secondary);
font-size: var(--ha-font-size-m);
}
.installed ha-svg-icon {
--mdc-icon-size: 16px;
color: var(--ha-color-on-success-normal);
}
`;
}
@@ -1,7 +1,14 @@
import { mdiArrowUpBoldCircle, mdiPuzzle } from "@mdi/js";
import {
mdiAlertDecagramOutline,
mdiArrowUpBoldCircle,
mdiArrowUpBoldCircleOutline,
mdiFlask,
mdiPuzzle,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
import { navigate } from "../../../common/navigate";
import { caseInsensitiveStringCompare } from "../../../common/string/compare";
@@ -10,6 +17,7 @@ import type { HassioAddonRepository } from "../../../data/hassio/addon";
import type { StoreAddon } from "../../../data/supervisor/store";
import type { HomeAssistant } from "../../../types";
import "./components/supervisor-apps-card-content";
import type { AppTag } from "./components/supervisor-apps-card-content";
import { filterAndSort } from "./components/supervisor-apps-filter";
import { supervisorAppsStyle } from "./resources/supervisor-apps-style";
@@ -54,21 +62,29 @@ export class SupervisorAppsRepositoryEl extends LitElement {
<div class="content">
<h1>${repo.name}</h1>
<div class="card-group">
${addons.map(
(addon) => html`
${addons.map((addon) => {
const tags = this._getAppTags(addon);
return html`
<ha-card
outlined
.addon=${addon}
class=${addon.available ? "" : "not_available"}
@click=${this._addonTapped}
>
<div class="card-content">
<div
class=${classMap({
"card-content": true,
"has-footer": tags.length > 0 || addon.installed,
})}
>
<supervisor-apps-card-content
.hass=${this.hass}
.title=${addon.name}
.stage=${addon.stage}
.description=${addon.description}
.available=${addon.available}
.installed=${addon.installed}
.tags=${tags}
.icon=${addon.installed && addon.update_available
? mdiArrowUpBoldCircle
: mdiPuzzle}
@@ -108,8 +124,8 @@ export class SupervisorAppsRepositoryEl extends LitElement {
></supervisor-apps-card-content>
</div>
</ha-card>
`
)}
`;
})}
</div>
</div>
`;
@@ -119,6 +135,32 @@ export class SupervisorAppsRepositoryEl extends LitElement {
navigate(`/config/app/${ev.currentTarget.addon.slug}/info?store=true`);
}
private _getAppTags(addon: StoreAddon): AppTag[] {
const labels: AppTag[] = [];
if (addon.installed && addon.update_available) {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.state.update_available`
),
variant: "brand",
iconPath: mdiArrowUpBoldCircleOutline,
});
}
if (addon.stage !== "stable") {
labels.push({
label: this.hass.localize(
`ui.panel.config.apps.dashboard.capability.stages.${addon.stage}`
),
variant: addon.stage === "experimental" ? "warning" : "danger",
iconPath:
addon.stage === "experimental" ? mdiFlask : mdiAlertDecagramOutline,
});
}
return labels;
}
static get styles(): CSSResultGroup {
return [
supervisorAppsStyle,
@@ -127,6 +169,9 @@ export class SupervisorAppsRepositoryEl extends LitElement {
cursor: pointer;
overflow: hidden;
}
.card-content.has-footer {
padding: var(--ha-space-4) var(--ha-space-4) var(--ha-space-2);
}
.not_available {
opacity: 0.6;
}
+101 -108
View File
@@ -12,22 +12,32 @@ import {
import { computeAreaName } from "../../../common/entity/compute_area_name";
import { fireEvent } from "../../../common/dom/fire_event";
import "../../../components/ha-adaptive-dialog";
import "../../../components/ha-list";
import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon";
import {
areasContext,
internationalizationContext,
} from "../../../data/context";
import type { SceneEntities } from "../../../data/scene";
import { showSceneEditor } from "../../../data/scene";
import "../../../dialogs/add-to/ha-add-to-action-list";
import type {
AddToActionListActionSelectedEvent,
AddToActionListItem,
AddToActionListSection,
} from "../../../dialogs/add-to/ha-add-to-action-list";
import {
addToActionHandler,
type AddToActionKey,
} from "../../../dialogs/more-info/add-to";
createAddToSceneEntities,
type AddToAutomationScriptActionKey,
} from "../../../dialogs/add-to/add-to";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { AreaAddToDialogParams } from "./show-dialog-area-add-to";
type AreaAddToAction =
| (AddToActionListItem & {
type: "automation";
key: AddToAutomationScriptActionKey;
})
| (AddToActionListItem & { type: "scene" });
@customElement("dialog-area-add-to")
class DialogAreaAddTo extends LitElement {
@state()
@@ -65,7 +75,12 @@ class DialogAreaAddTo extends LitElement {
<ha-adaptive-dialog
.open=${this._open}
header-title=${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.title"
"ui.dialogs.more_info_control.add_to.title",
{
target:
computeAreaName(this._areas[this._params.areaId]) ||
this._params.areaId,
}
)}
@closed=${this._dialogClosed}
>
@@ -79,108 +94,96 @@ class DialogAreaAddTo extends LitElement {
return nothing;
}
const area = this._areas[this._params.areaId];
const areaName = computeAreaName(area) || this._params.areaId;
return html`
<h3 class="section-header">
${this._i18n.localize(
const sections: AddToActionListSection<AreaAddToAction>[] = [
{
title: this._i18n.localize(
"ui.panel.config.devices.automation.automations_heading"
)}
</h3>
<ha-list>
${this._renderActionItem(
"automation_trigger",
mdiRobotOutline,
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
areaName
)}
${this._renderActionItem(
"automation_condition",
mdiPlaylistCheck,
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
areaName
)}
${this._renderActionItem(
"automation_action",
mdiPlayCircleOutline,
"ui.dialogs.more_info_control.add_to.actions.automation_action",
areaName
)}
</ha-list>
<h3 class="section-header">
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
</h3>
<ha-list>
${this._renderActionItem(
"script_action",
mdiScriptTextOutline,
"ui.dialogs.more_info_control.add_to.actions.script_action",
areaName
)}
</ha-list>
${this._renderSceneSection(areaName)}
`;
}
),
actions: [
{
type: "automation",
key: "automation_trigger",
iconPath: mdiRobotOutline,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
),
},
{
type: "automation",
key: "automation_condition",
iconPath: mdiPlaylistCheck,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
),
},
{
type: "automation",
key: "automation_action",
iconPath: mdiPlayCircleOutline,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
),
},
],
},
{
title: this._i18n.localize(
"ui.panel.config.devices.script.scripts_heading"
),
actions: [
{
type: "automation",
key: "script_action",
iconPath: mdiScriptTextOutline,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.script_action"
),
},
],
},
];
private _renderSceneSection(areaName: string) {
if (!this._params?.entityIds.length) {
return nothing;
if (this._params.canCreateScene && this._params.entityIds.length) {
sections.push({
title: this._i18n.localize(
"ui.panel.config.devices.scene.scenes_heading"
),
actions: [
{
type: "scene",
iconPath: mdiPalette,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.scene"
),
},
],
});
}
return html`
<h3 class="section-header">
${this._i18n.localize("ui.panel.config.devices.scene.scenes_heading")}
</h3>
<ha-list>
<ha-list-item
graphic="icon"
@click=${this._handleCreateScene}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.scene",
{ target: areaName }
)}
</ha-list-item>
</ha-list>
<ha-add-to-action-list
.sections=${sections}
@add-to-list-action-selected=${this._handleActionSelected}
></ha-add-to-action-list>
`;
}
private _renderActionItem(
key: AddToActionKey,
path: string,
translationKey:
| "ui.dialogs.more_info_control.add_to.actions.automation_trigger"
| "ui.dialogs.more_info_control.add_to.actions.automation_condition"
| "ui.dialogs.more_info_control.add_to.actions.automation_action"
| "ui.dialogs.more_info_control.add_to.actions.script_action",
areaName: string
private _handleActionSelected(
ev: AddToActionListActionSelectedEvent<AreaAddToAction>
) {
return html`
<ha-list-item
graphic="icon"
data-type=${key}
@click=${this._handleAction}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${path}></ha-svg-icon>
${this._i18n.localize(translationKey, { target: areaName })}
</ha-list-item>
`;
}
private _handleAction(ev: Event) {
if (!this._params) {
return;
}
const key = (ev.currentTarget as HTMLElement).dataset
.type as AddToActionKey;
const { action } = ev.detail;
if (action.type === "scene") {
this._handleCreateScene();
return;
}
this.closeDialog();
addToActionHandler(key, { area_id: this._params.areaId });
addToActionHandler(action.key, { area_id: this._params.areaId });
}
private _handleCreateScene() {
@@ -188,13 +191,11 @@ class DialogAreaAddTo extends LitElement {
return;
}
const entities: SceneEntities = {};
for (const entityId of this._params.entityIds) {
entities[entityId] = "";
}
this.closeDialog();
showSceneEditor({ entities }, this._params.areaId);
showSceneEditor(
{ entities: createAddToSceneEntities(this._params.entityIds) },
this._params.areaId
);
}
static get styles(): CSSResultGroup {
@@ -205,14 +206,6 @@ class DialogAreaAddTo extends LitElement {
ha-adaptive-dialog {
--dialog-content-padding: 0;
}
.section-header {
padding: var(--ha-space-2) var(--ha-space-4) 0;
margin: 0;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
color: var(--secondary-text-color);
}
`,
];
}
@@ -48,8 +48,6 @@ import type { AreaRegistryDetailDialogParams } from "./show-dialog-area-registry
const cropOptions: CropOptions = {
round: false,
type: "image/jpeg",
quality: 0.75,
};
const SENSOR_DOMAINS = ["sensor"];
+11 -4
View File
@@ -60,6 +60,7 @@ import type { SceneEntity } from "../../../data/scene";
import type { ScriptEntity } from "../../../data/script";
import type { RelatedResult } from "../../../data/search";
import { findRelated } from "../../../data/search";
import { filterAddToSceneEntityIds } from "../../../dialogs/add-to/add-to";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import "../../../layouts/hass-error-screen";
@@ -145,8 +146,6 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg: EntityRegistryEntry[] = [];
@@ -441,7 +440,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
.path=${mdiPlus}
></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.more_info_control.add_to.title"
"ui.dialogs.more_info_control.add_to.item"
)}
</ha-button>`
: nothing}
@@ -783,9 +782,17 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
if (!area) {
return;
}
const sceneEntityIds = filterAddToSceneEntityIds(
this._areaEntityIds,
this._entityReg,
this.hass.states
);
showAreaAddToDialog(this, {
areaId: area.area_id,
entityIds: this._areaEntityIds,
entityIds: sceneEntityIds,
canCreateScene:
isComponentLoaded(this.hass.config, "scene") &&
sceneEntityIds.length > 0,
});
}
@@ -13,8 +13,6 @@ class HaConfigAreas extends HassRouterPage {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
routes: {
@@ -37,7 +35,6 @@ class HaConfigAreas extends HassRouterPage {
pageEl.narrow = this.narrow;
pageEl.isWide = this.isWide;
pageEl.showAdvanced = this.showAdvanced;
pageEl.route = this.routeTail;
}
}
@@ -3,6 +3,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
export interface AreaAddToDialogParams {
areaId: string;
entityIds: string[];
canCreateScene: boolean;
}
export const loadAreaAddToDialog = () => import("./dialog-area-add-to");
@@ -107,7 +107,7 @@ export default class HaAutomationActionEditor extends LitElement {
ev.stopPropagation();
const value = {
...(this.action.alias ? { alias: this.action.alias } : {}),
...(this.action.comment ? { comment: this.action.comment } : {}),
...(this.action.note ? { note: this.action.note } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
@@ -297,8 +297,8 @@ export default class HaAutomationActionRow extends LitElement {
?.target
: undefined;
const commentTooltipText = truncateWithEllipsis(
this.action.comment?.trim() || "",
const noteTooltipText = truncateWithEllipsis(
this.action.note?.trim() || "",
250
);
@@ -334,21 +334,23 @@ export default class HaAutomationActionRow extends LitElement {
? this._renderTargets(
target,
actionHasTarget && !this._isNew,
serviceTargetSpec
serviceTargetSpec,
type !== "device_id"
)
: nothing}
${commentTooltipText
${noteTooltipText
? html`
<ha-svg-icon
id="comment-icon"
id="note-icon"
tabindex="0"
.path=${mdiCommentTextOutline}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
)}
class="comment-indicator"
class="note-indicator"
></ha-svg-icon
><ha-tooltip for="comment-icon"
><p>${commentTooltipText}</p></ha-tooltip
><ha-tooltip for="note-icon"
><p>${noteTooltipText}</p></ha-tooltip
>
`
: nothing}
@@ -407,11 +409,11 @@ export default class HaAutomationActionRow extends LitElement {
)
)}
</ha-dropdown-item>
<ha-dropdown-item value="edit_comment">
<ha-dropdown-item value="edit_note">
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.action.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.action.note ? "edit" : "add"}`
)
)}
</ha-dropdown-item>
@@ -721,13 +723,14 @@ export default class HaAutomationActionRow extends LitElement {
(
target?: HassServiceTarget,
targetRequired = false,
targetSpec?: TargetSelector["target"]
targetSpec?: TargetSelector["target"],
interactive = false
) =>
html`<ha-automation-row-targets
.hass=${this.hass}
.target=${target}
.targetRequired=${targetRequired}
.selector=${targetSpec ? { target: targetSpec } : undefined}
.interactive=${interactive}
></ha-automation-row-targets>`
);
@@ -941,25 +944,25 @@ export default class HaAutomationActionRow extends LitElement {
}
};
private _editCommentAction = async (): Promise<void> => {
const comment = await showPromptDialog(this, {
private _editNoteAction = async (): Promise<void> => {
const note = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.action.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.action.note ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
),
inputType: "string",
defaultValue: this.action.comment,
defaultValue: this.action.note,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
if (note !== null) {
const value = { ...this.action };
if (comment === "") {
delete value.comment;
if (note === "") {
delete value.note;
} else {
value.comment = comment;
value.note = note;
}
fireEvent(this, "value-changed", {
value,
@@ -1089,7 +1092,7 @@ export default class HaAutomationActionRow extends LitElement {
rename: () => {
this._renameAction();
},
editComment: this._editCommentAction,
editNote: this._editNoteAction,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -1185,8 +1188,8 @@ export default class HaAutomationActionRow extends LitElement {
case "rename":
this._renameAction();
break;
case "edit_comment":
this._editCommentAction();
case "edit_note":
this._editNoteAction();
break;
case "duplicate":
this._duplicateAction();
@@ -30,10 +30,10 @@ import type {
LocalizeFunc,
LocalizeKeys,
} from "../../../common/translations/localize";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
import { computeRTL } from "../../../common/util/compute_rtl";
import { debounce } from "../../../common/util/debounce";
import { deepEqual } from "../../../common/util/deep-equal";
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
import "../../../components/entity/state-badge";
import "../../../components/ha-bottom-sheet";
import "../../../components/ha-button";
@@ -134,8 +134,8 @@ import {
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
getAddAutomationElementTargetFromQuery,
PASTE_VALUE,
getAddAutomationElementTargetFromQuery,
} from "./show-add-automation-element-dialog";
import { getTargetText } from "./target/get_target_text";
@@ -795,37 +795,33 @@ class DialogAddAutomationElement
class="paste"
@click=${this._paste}
>
<div class="shortcut-label">
<div class="label">
<div>
${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.paste`
)}
</div>
<div class="supporting-text">
${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
)}
</div>
</div>
${!this._narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>V</span>
</span>`
: nothing}
<div slot="headline" class="label">
${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.paste`
)}
</div>
<div slot="supporting-text">
${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
)}
</div>
${!this._narrow
? html`<span slot="end" class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>V</span>
</span>`
: nothing}
<ha-svg-icon
slot="start"
.path=${mdiContentPaste}
@@ -2546,23 +2542,16 @@ class DialogAddAutomationElement
ha-svg-icon.plus {
color: var(--primary-color);
}
.shortcut-label {
display: flex;
gap: var(--ha-space-3);
justify-content: space-between;
}
.shortcut-label .supporting-text {
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
}
.shortcut-label .shortcut {
.shortcut {
--mdc-icon-size: var(--ha-space-3);
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 2px;
margin-right: var(--ha-space-4);
}
.shortcut-label .shortcut span {
.shortcut span {
font-size: var(--ha-font-size-s);
font-family: var(--ha-font-family-code);
color: var(--ha-color-text-secondary);
@@ -95,7 +95,6 @@ class DialogAutomationMode extends LitElement implements HassDialog {
.value=${this._newMode}
@value-changed=${this._modeChanged}
.maxColumns=${1}
.hass=${this.hass}
></ha-select-box>
${isMaxMode(this._newMode)
@@ -123,7 +123,7 @@ export default class HaAutomationConditionEditor extends LitElement {
ev.stopPropagation();
const value = {
...(this.condition.alias ? { alias: this.condition.alias } : {}),
...(this.condition.comment ? { comment: this.condition.comment } : {}),
...(this.condition.note ? { note: this.condition.note } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
@@ -1,7 +1,6 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAlert,
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
@@ -26,7 +25,7 @@ import type {
} from "home-assistant-js-websocket";
import { dump } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
@@ -54,25 +53,18 @@ import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-tooltip";
import "../../../../components/ha-trigger-icon";
import type {
AutomationClipboard,
AutomationConfig,
Condition,
ConditionSidebarConfig,
PlatformCondition,
TriggerCondition,
} from "../../../../data/automation";
import {
automationConfigContext,
isCondition,
subscribeCondition,
testCondition,
} from "../../../../data/automation";
import {
describeCondition,
getTriggerInfos,
} from "../../../../data/automation_i18n";
import { describeCondition } from "../../../../data/automation_i18n";
import type { ConditionDescriptions } from "../../../../data/condition";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import {
@@ -92,7 +84,6 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../ha-automation-editor-warning";
import "../ha-trigger-id-chip";
import { overflowStyles, rowStyles } from "../styles";
import "../target/ha-automation-row-targets";
import "./ha-automation-condition-editor";
@@ -164,11 +155,10 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _selected = false;
@state() private _liveTestResult: LiveTestState = "unknown";
@state()
@consume({ context: automationConfigContext, subscribe: true })
private _automationConfig?: AutomationConfig;
@state() private _liveTestResult: {
state: LiveTestState;
message?: string;
} = { state: "unknown" };
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
@@ -216,45 +206,57 @@ export default class HaAutomationConditionRow extends LitElement {
const conditionTargetSpec =
this.conditionDescriptions[this.condition.condition]?.target;
const commentTooltipText = truncateWithEllipsis(
this.condition.comment?.trim() || "",
const noteTooltipText = truncateWithEllipsis(
this.condition.note?.trim() || "",
250
);
return html`
<ha-condition-icon
slot="leading-icon"
.hass=${this.hass}
.condition=${this.condition.condition}
></ha-condition-icon>
<div id="condition-icon" class="icon-badge-wrapper" slot="leading-icon">
<ha-condition-icon
.hass=${this.hass}
.condition=${this.condition.condition}
></ha-condition-icon>
${this.optionsInSidebar && this.condition.condition !== "trigger"
? html`<ha-automation-row-live-test
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
)}
></ha-automation-row-live-test>`
: nothing}
</div>
${this.optionsInSidebar &&
this.condition.condition !== "trigger" &&
this._liveTestResult.message
? html`<ha-tooltip for="condition-icon" slot="leading-icon"
>${this._liveTestResult.message}</ha-tooltip
>`
: nothing}
<h3 slot="header">
${this.condition.condition === "trigger"
? this._renderTriggerConditionDescription(
this.condition as TriggerCondition
)
: capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
${target !== undefined || (descriptionHasTarget && !this._isNew)
? this._renderTargets(
target,
descriptionHasTarget && !this._isNew,
conditionTargetSpec
conditionTargetSpec,
this.condition.condition !== "device"
)
: nothing}
${this.condition.comment?.trim()
${this.condition.note?.trim()
? html`
<ha-svg-icon
id="comment-icon"
id="note-icon"
tabindex="0"
.path=${mdiCommentTextOutline}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
)}
class="comment-indicator"
class="note-indicator"
></ha-svg-icon>
<ha-tooltip for="comment-icon"
><p>${commentTooltipText}</p></ha-tooltip
>
<ha-tooltip for="note-icon"><p>${noteTooltipText}</p></ha-tooltip>
`
: nothing}
</h3>
@@ -304,11 +306,11 @@ export default class HaAutomationConditionRow extends LitElement {
)
)}
</ha-dropdown-item>
<ha-dropdown-item value="edit_comment">
<ha-dropdown-item value="edit_note">
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.condition.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.condition.note ? "edit" : "add"}`
)
)}
</ha-dropdown-item>
@@ -546,16 +548,7 @@ export default class HaAutomationConditionRow extends LitElement {
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
>${this._renderRow()}
<ha-automation-row-live-test
slot="icons"
.state=${this.condition.condition !== "trigger"
? this._liveTestResult
: "unknown"}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult : "unknown"}`
)}
></ha-automation-row-live-test
></ha-automation-row>`
</ha-automation-row>`
: html`
<ha-expansion-panel
left-chevron
@@ -585,113 +578,18 @@ export default class HaAutomationConditionRow extends LitElement {
`;
}
private _getTriggerInfos = memoizeOne(getTriggerInfos);
private _renderTriggerConditionDescription(condition: TriggerCondition) {
const ids = ensureArray(condition.id ?? [])
.map((id) => (typeof id === "string" ? id : String(id)))
.filter((id) => id !== "");
const prefix = capitalizeFirstLetter(
this.hass
.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.description.full",
{ id: "" }
)
.trim()
);
if (!ids.length) {
return html`${prefix}
<div class="trigger warning">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.description.no_trigger"
)}
</div>`;
}
const triggerInfos = this._getTriggerInfos(
ensureArray(this._automationConfig?.triggers || []),
this.hass,
this._entityReg
);
const infoById = new Map(triggerInfos.map((info) => [info.id, info]));
return html`${prefix}
${ids.map((id) => {
const info = infoById.get(id);
if (!info) {
return html`<div class="trigger">
<ha-trigger-id-chip id=${`trigger-${id}`} warning .triggerId=${id}>
<ha-svg-icon slot="start" .path=${mdiAlert}></ha-svg-icon>
</ha-trigger-id-chip>
${ids.length < 4
? html`<span
>${this.hass.localize("state.default.unavailable")}</span
>`
: nothing}
<ha-tooltip .for=${`trigger-${id}`}>
${ids.length >= 4
? html`<div>
${this.hass.localize("state.default.unavailable")}
</div>`
: nothing}
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.unavailable_info",
{ id: html`<b>${id}</b>` }
)}
</ha-tooltip>
</div>`;
}
const triggerIcon = html`<ha-trigger-icon
.slot=${ids.length < 4 ? "start" : ""}
.hass=${this.hass}
.trigger=${info.triggerType}
></ha-trigger-icon>`;
const isDuplicateId = info.count > 1;
return html`
<div class="trigger">
${ids.length < 4 ? triggerIcon : nothing}
<ha-trigger-id-chip
id=${`trigger-${id}`}
.triggerId=${id}
.warning=${isDuplicateId}
>
${isDuplicateId
? html`<ha-svg-icon slot="start" .path=${mdiAlert}></ha-svg-icon>`
: nothing}
</ha-trigger-id-chip>
${ids.length < 4
? html`<span>${info.label}</span>`
: html`<ha-tooltip .for=${`trigger-${id}`}></ha-tooltip>`}
${isDuplicateId || ids.length >= 4
? html`<ha-tooltip .for=${`trigger-${id}`}>
${ids.length >= 4
? html`<div>${triggerIcon}${info.label}</div>`
: nothing}
${isDuplicateId
? this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate_id_warning"
)
: nothing}
</ha-tooltip>`
: nothing}
</div>
`;
})}`;
}
private _renderTargets = memoizeOne(
(
target?: HassServiceTarget,
targetRequired = false,
targetSpec?: TargetSelector["target"]
targetSpec?: TargetSelector["target"],
interactive = false
) =>
html`<ha-automation-row-targets
.hass=${this.hass}
.target=${target}
.targetRequired=${targetRequired}
.selector=${targetSpec ? { target: targetSpec } : undefined}
.interactive=${interactive}
></ha-automation-row-targets>`
);
@@ -736,7 +634,12 @@ export default class HaAutomationConditionRow extends LitElement {
}
private _resetSubscription() {
this._liveTestResult = "unknown";
this._liveTestResult = {
state: "unknown",
message: this.hass.localize(
"ui.panel.config.automation.editor.conditions.live_test_state.unknown"
),
};
if (this._conditionUnsub) {
this._conditionUnsub.then((unsub) => unsub());
this._conditionUnsub = undefined;
@@ -761,7 +664,12 @@ export default class HaAutomationConditionRow extends LitElement {
if (result.error) {
this._handleLiveTestError(result.error);
} else {
this._liveTestResult = result.result ? "pass" : "fail";
this._liveTestResult = {
state: result.result ? "pass" : "fail",
message: this.hass.localize(
`ui.panel.config.automation.editor.conditions.testing_${result.result ? "pass" : "error"}`
),
};
}
},
this.condition
@@ -778,7 +686,12 @@ export default class HaAutomationConditionRow extends LitElement {
private _handleLiveTestError(error: any) {
const invalid =
typeof error !== "string" && error.code === "invalid_format";
this._liveTestResult = invalid ? "invalid" : "unknown";
this._liveTestResult = {
state: invalid ? "invalid" : "unknown",
message: this.hass.localize(
`ui.panel.config.automation.editor.conditions.${invalid ? "invalid_condition" : "live_test_state.unknown"}`
),
};
}
private _onValueChange(event: CustomEvent) {
@@ -945,25 +858,25 @@ export default class HaAutomationConditionRow extends LitElement {
}
};
private _editCommentCondition = async (): Promise<void> => {
const comment = await showPromptDialog(this, {
private _editNoteCondition = async (): Promise<void> => {
const note = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.condition.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.condition.note ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
),
inputType: "string",
defaultValue: this.condition.comment,
defaultValue: this.condition.note,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
if (note !== null) {
const value = { ...this.condition };
if (comment === "") {
delete value.comment;
if (note === "") {
delete value.note;
} else {
value.comment = comment;
value.note = note;
}
fireEvent(this, "value-changed", {
value,
@@ -1118,7 +1031,7 @@ export default class HaAutomationConditionRow extends LitElement {
rename: () => {
this._renameCondition();
},
editComment: this._editCommentCondition,
editNote: this._editNoteCondition,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -1190,8 +1103,8 @@ export default class HaAutomationConditionRow extends LitElement {
case "rename":
this._renameCondition();
break;
case "edit_comment":
this._editCommentCondition();
case "edit_note":
this._editNoteCondition();
break;
case "duplicate":
this._duplicateCondition();
@@ -1224,26 +1137,7 @@ export default class HaAutomationConditionRow extends LitElement {
}
static get styles(): CSSResultGroup {
return [
rowStyles,
overflowStyles,
css`
.trigger {
display: flex;
align-items: center;
gap: var(--ha-space-2);
background-color: var(--ha-color-fill-neutral-normal-resting);
border-radius: var(--ha-border-radius-md);
padding-inline: var(--ha-space-2);
color: var(--ha-color-on-neutral-normal);
height: 32px;
}
.trigger.warning {
background-color: var(--ha-color-fill-warning-normal-resting);
color: var(--ha-color-on-warning-normal);
}
`,
];
return [rowStyles, overflowStyles];
}
}
@@ -22,7 +22,7 @@ import type { HomeAssistant } from "../../../../../types";
const numericStateConditionStruct = object({
alias: optional(string()),
comment: optional(string()),
note: optional(string()),
condition: literal("numeric_state"),
entity_id: optional(string()),
attribute: optional(string()),
@@ -25,7 +25,7 @@ import type { ConditionElement } from "../ha-automation-condition-row";
const stateConditionStruct = object({
alias: optional(string()),
comment: optional(string()),
note: optional(string()),
condition: literal("state"),
entity_id: optional(string()),
attribute: optional(string()),
@@ -40,37 +40,6 @@ export class HaSunCondition extends LitElement implements ConditionElement {
private _schema = memoizeOne(
(localize: LocalizeFunc, formType: FormType) =>
[
...(["between", "before"].includes(formType)
? [
{
name: "before",
type: "select",
default: BEFORE_DEFAULT,
options: [
[
"sunrise",
localize(
"ui.panel.config.automation.editor.conditions.type.sun.sunrise"
),
],
[
"sunset",
localize(
"ui.panel.config.automation.editor.conditions.type.sun.sunset"
),
],
],
},
{
name: "before_offset",
selector: {
duration: {
allow_negative: true,
},
},
},
]
: []),
...(["between", "after"].includes(formType)
? [
{
@@ -102,6 +71,37 @@ export class HaSunCondition extends LitElement implements ConditionElement {
},
]
: []),
...(["between", "before"].includes(formType)
? [
{
name: "before",
type: "select",
default: BEFORE_DEFAULT,
options: [
[
"sunrise",
localize(
"ui.panel.config.automation.editor.conditions.type.sun.sunrise"
),
],
[
"sunset",
localize(
"ui.panel.config.automation.editor.conditions.type.sun.sunset"
),
],
],
},
{
name: "before_offset",
selector: {
duration: {
allow_negative: true,
},
},
},
]
: []),
] as const
);
@@ -1,31 +1,26 @@
import { consume } from "@lit/context";
import { mdiAlert } from "@mdi/js";
import { css, html, LitElement, nothing } from "lit";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-select";
import "../../../../../components/item/ha-list-item-option";
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
import "../../../../../components/list/ha-list-selectable";
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
import type { HaListSelectedDetail } from "../../../../../components/list/types";
import {
automationConfigContext,
flattenTriggers,
type AutomationConfig,
type Trigger,
type TriggerCondition,
} from "../../../../../data/automation";
import {
getTriggerInfos,
type TriggerInfo,
} from "../../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../../data/context";
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../../../types";
import "../../ha-trigger-id-chip";
const getTriggersIds = (triggers: Trigger[]): string[] => {
const triggerIds = flattenTriggers(triggers)
.map((t) => ("id" in t ? t.id : undefined))
.filter(Boolean) as string[];
return Array.from(new Set(triggerIds));
};
@customElement("ha-automation-condition-trigger")
export class HaTriggerCondition extends LitElement {
@@ -35,25 +30,9 @@ export class HaTriggerCondition extends LitElement {
@property({ type: Boolean }) public disabled = false;
@state()
@consume({ context: automationConfigContext, subscribe: true })
private _automationConfig?: AutomationConfig;
@state() private _triggerIds: string[] = [];
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
private _entityReg: EntityRegistryEntry[] = [];
private _triggerInfos = memoizeOne(
(
triggers: AutomationConfig["triggers"] | undefined,
entityReg: EntityRegistryEntry[]
): TriggerInfo[] =>
getTriggerInfos(
triggers ? ensureArray(triggers) : undefined,
this.hass,
entityReg
)
);
private _unsub?: UnsubscribeFunc;
public static get defaultConfig(): TriggerCondition {
return {
@@ -62,146 +41,89 @@ export class HaTriggerCondition extends LitElement {
};
}
private _schema = memoizeOne(
(triggerIds: string[]) =>
[
{
name: "id",
selector: {
select: {
multiple: true,
options: triggerIds,
},
},
required: true,
},
] as const
);
connectedCallback() {
super.connectedCallback();
const details = { callback: (config) => this._automationUpdated(config) };
fireEvent(this, "subscribe-automation-config", details);
this._unsub = (details as any).unsub;
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._unsub) {
this._unsub();
}
}
protected render() {
const selectedIds = ensureArray(this.condition.id || []).filter(
(id): id is string => typeof id === "string" && id !== ""
);
const triggerInfos = this._triggerInfos(
this._automationConfig?.triggers,
this._entityReg
);
if (!triggerInfos.length && !selectedIds.length) {
return html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
)}
</ha-alert>
`;
if (!this._triggerIds.length) {
return this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
);
}
const schema = this._schema(this._triggerIds);
return html`
<ha-list-selectable @ha-list-selected=${this._valueChanged} multi>
${this._renderOptions(selectedIds, triggerInfos)}
</ha-list-selectable>
<ha-form
.schema=${schema}
.data=${this.condition}
.hass=${this.hass}
.disabled=${this.disabled}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _renderOptions(selectedIds: string[], triggerInfos: TriggerInfo[]) {
const unknownTriggerIds = selectedIds.filter(
(id) => !triggerInfos.some((info) => info.id === id)
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.trigger.${schema.name}`
);
const alertIcon = html`<ha-svg-icon
slot="start"
.path=${mdiAlert}
></ha-svg-icon>`;
return html`
${unknownTriggerIds.map(
(id) => html`
<ha-list-item-option
.value=${id}
.selected=${true}
appearance="checkbox"
>
<div class="option" slot="headline">
<ha-trigger-id-chip
id=${`trigger-${id}`}
warning
.triggerId=${id}
>
${alertIcon}
</ha-trigger-id-chip>
${this.hass.localize("state.default.unavailable")}
<ha-tooltip .for=${`trigger-${id}`}>
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.unavailable_info",
{ id: html`<b>${id}</b>` }
)}
</ha-tooltip>
</div>
</ha-list-item-option>
`
)}
${triggerInfos.map(
(info) => html`
<ha-list-item-option
.value=${info.id}
.selected=${selectedIds.includes(info.id)}
appearance="checkbox"
>
<div class="option" slot="headline">
<ha-trigger-id-chip
id=${`trigger-${info.id}`}
.warning=${info.count > 1}
.triggerId=${info.id}
>
${info.count > 1 ? alertIcon : nothing}
</ha-trigger-id-chip>
${info.label}${info.count > 1
? html`<ha-tooltip .for=${`trigger-${info.id}`}
>${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.duplicated_info"
)}</ha-tooltip
>`
: nothing}
</div>
</ha-list-item-option>
`
)}
`;
private _automationUpdated(config?: AutomationConfig) {
this._triggerIds = config?.triggers
? getTriggersIds(ensureArray(config.triggers))
: [];
}
private _valueChanged(ev: CustomEvent<HaListSelectedDetail>): void {
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (
!ev.detail.diff ||
(!ev.detail.diff?.added.size && !ev.detail.diff?.removed.size)
) {
return;
}
const newValue = ev.detail.value;
const ids = ensureArray(this.condition.id || []);
const valueSet = ev.detail.diff.added.size
? ev.detail.diff.added
: ev.detail.diff.removed;
const index = valueSet.values().next().value;
if (index === undefined) {
return;
}
const triggerId = (
(ev.currentTarget as HaListSelectable).items[index] as HaListItemOption
).value;
if (triggerId === undefined || triggerId === "") {
return;
}
if (ev.detail.diff.added.size) {
ids.push(triggerId);
} else {
const removeIndex = ids.indexOf(triggerId);
if (removeIndex > -1) {
ids.splice(removeIndex, 1);
if (typeof newValue.id === "string") {
if (!this._triggerIds.some((id) => id === newValue.id)) {
newValue.id = "";
}
} else if (Array.isArray(newValue.id)) {
newValue.id = newValue.id.filter((_id) =>
this._triggerIds.some((id) => id === _id)
);
if (!newValue.id.length) {
newValue.id = "";
}
}
fireEvent(this, "value-changed", { value: { ...this.condition, id: ids } });
fireEvent(this, "value-changed", { value: newValue });
}
static styles = css`
.option {
display: flex;
align-items: center;
gap: var(--ha-space-1);
color: var(--ha-color-on-neutral-normal);
}
`;
}
declare global {
@@ -1,5 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { provide } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiCog,
@@ -21,9 +20,10 @@ import {
mdiTransitConnection,
mdiUndo,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -31,7 +31,6 @@ import { goBack, navigate } from "../../../common/navigate";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
@@ -46,7 +45,6 @@ import type {
Trigger,
} from "../../../data/automation";
import {
automationConfigContext,
deleteAutomation,
fetchAutomationFileConfig,
getAutomationEditorInitData,
@@ -74,12 +72,13 @@ import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { haStyle } from "../../../resources/styles";
import type { Entries, ValueChangedEvent } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showEditorToast } from "./editor-toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import { ADD_AUTOMATION_ELEMENT_QUERY_PARAM } from "./show-add-automation-element-dialog";
import "./blueprint-automation-editor";
import { showEditorToast } from "./editor-toast";
import type { EditorDomainHooks } from "./ha-automation-script-editor-mixin";
import {
AutomationScriptEditorMixin,
@@ -87,7 +86,7 @@ import {
} from "./ha-automation-script-editor-mixin";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import { ADD_AUTOMATION_ELEMENT_QUERY_PARAM } from "./show-add-automation-element-dialog";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
declare global {
interface HTMLElementTagNameMap {
@@ -95,6 +94,10 @@ declare global {
}
// for fire event
interface HASSDomEvents {
"subscribe-automation-config": {
callback: (config: AutomationConfig) => void;
unsub?: UnsubscribeFunc;
};
"ui-mode-not-available": Error;
"move-down": undefined;
"move-up": undefined;
@@ -122,9 +125,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
@provide({ context: automationConfigContext })
@state()
protected config?: AutomationConfig;
private _configSubscriptions: Record<
string,
(config?: AutomationConfig) => void
> = {};
private _configSubscriptionsId = 1;
private _newAutomationId?: string;
@@ -398,7 +404,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
</ha-svg-icon>
</ha-dropdown-item>
</ha-dropdown>
<div class=${this.mode === "yaml" ? "yaml-mode" : ""}>
<div
class=${this.mode === "yaml" ? "yaml-mode" : ""}
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
${this.mode === "gui"
? html`
<div>
@@ -629,6 +638,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
) {
this._setEntityId();
}
if (changedProps.has("config")) {
Object.values(this._configSubscriptions).forEach((sub) =>
sub(this.config)
);
}
}
private _setEntityId() {
@@ -1006,6 +1021,15 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
}
private _subscribeAutomationConfig(ev) {
const id = this._configSubscriptionsId++;
this._configSubscriptions[id] = ev.detail.callback;
ev.detail.unsub = () => {
delete this._configSubscriptions[id];
};
ev.detail.callback(this.config);
}
protected supportedShortcuts(): SupportedShortcuts {
return {
s: () => this._handleSaveAutomation(),
@@ -6,9 +6,9 @@ import "../../../components/ha-button";
import "../../../components/ha-settings-row";
import { internationalizationContext } from "../../../data/context";
@customElement("ha-automation-comment")
export class HaAutomationComment extends LitElement {
@property() public comment!: string;
@customElement("ha-automation-note")
export class HaAutomationNote extends LitElement {
@property() public note!: string;
@state()
@consume({ context: internationalizationContext, subscribe: true })
@@ -18,9 +18,9 @@ export class HaAutomationComment extends LitElement {
return html`
<ha-settings-row narrow>
<div class="heading" slot="heading">
<span class="title" id="comment-label">
<span class="title" id="note-label">
${this._i18n.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
)}
</span>
<ha-button
@@ -31,13 +31,13 @@ export class HaAutomationComment extends LitElement {
${this._i18n.localize("ui.common.edit")}
</ha-button>
</div>
<p aria-labelledby="comment-label">${this.comment}</p>
<p aria-labelledby="note-label">${this.note}</p>
</ha-settings-row>
`;
}
private _handleClick() {
fireEvent(this, "edit-comment");
fireEvent(this, "edit-note");
}
static styles = css`
@@ -70,10 +70,10 @@ export class HaAutomationComment extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"ha-automation-comment": HaAutomationComment;
"ha-automation-note": HaAutomationNote;
}
interface HASSDomEvents {
"edit-comment": undefined;
"edit-note": undefined;
}
}
@@ -27,8 +27,6 @@ class HaConfigAutomation extends HassRouterPage {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
@property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ attribute: false }) public automations: AutomationEntity[] = [];
@@ -79,7 +77,6 @@ class HaConfigAutomation extends HassRouterPage {
pageEl.narrow = this.narrow;
pageEl.isWide = this.isWide;
pageEl.route = this.routeTail;
pageEl.showAdvanced = this.showAdvanced;
pageEl.cloudStatus = this.cloudStatus;
if (this.hass) {
@@ -1,62 +0,0 @@
import { mdiPound } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-svg-icon";
/**
* Home Assistant trigger ID chip component
*
* @element ha-trigger-id-chip
* @extends {LitElement}
*
* @summary
* A small chip that displays an automation trigger ID prefixed with a hash icon.
*
* @slot start - Optional content rendered before the hash icon (usually an icon).
*
* @attr {string} trigger-id - The trigger ID to display.
* @attr {boolean} warning - Renders the chip with warning colors.
*/
@customElement("ha-trigger-id-chip")
export class HaTriggerIdChip extends LitElement {
@property({ attribute: "trigger-id" }) public triggerId!: string;
@property({ type: Boolean, reflect: true }) public warning = false;
protected render() {
return html`
<slot name="start"></slot>
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
<span>${this.triggerId}</span>
`;
}
static styles = css`
:host {
background-color: var(--card-background-color);
border-radius: var(--ha-border-radius-sm);
border: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-normal);
--mdc-icon-size: 16px;
display: inline-flex;
gap: var(--ha-space-1);
align-items: center;
color: var(--ha-color-on-neutral-normal);
padding: 0 var(--ha-space-1);
font-weight: var(--ha-font-weight-medium);
line-height: 20px;
height: 20px;
}
:host([warning]) {
border-color: var(--ha-color-border-warning-normal);
color: var(--ha-color-on-warning-normal);
background-color: var(--ha-color-fill-warning-quiet-resting);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-trigger-id-chip": HaTriggerIdChip;
}
}
@@ -32,11 +32,11 @@ import {
normalizeAutomationConfig,
} from "../../../data/automation";
import { getActionType, type Action } from "../../../data/script";
import { showEditorToast } from "./editor-toast";
import "./action/ha-automation-action";
import type HaAutomationAction from "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import type HaAutomationCondition from "./condition/ha-automation-condition";
import { showEditorToast } from "./editor-toast";
import { ManualEditorMixin } from "./ha-manual-editor-mixin";
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
import { manualEditorStyles, saveFabStyles } from "./styles";
@@ -142,8 +142,8 @@ export default class HaAutomationOptionRow extends LitElement {
`;
}
private _renderRow() {
const commentTooltipText = truncateWithEllipsis(
this.option?.comment?.trim() || "",
const noteTooltipText = truncateWithEllipsis(
this.option?.note?.trim() || "",
250
);
@@ -157,19 +157,18 @@ export default class HaAutomationOptionRow extends LitElement {
: this.hass.localize(
"ui.panel.config.automation.editor.actions.type.choose.default"
)}
${this.option?.comment?.trim()
${this.option?.note?.trim()
? html`
<ha-svg-icon
id="comment-icon"
id="note-icon"
tabindex="0"
.path=${mdiCommentTextOutline}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
)}
class="comment-indicator"
class="note-indicator"
></ha-svg-icon>
<ha-tooltip for="comment-icon"
><p>${commentTooltipText}</p></ha-tooltip
>
<ha-tooltip for="note-icon"><p>${noteTooltipText}</p></ha-tooltip>
`
: nothing}
</h3>
@@ -199,14 +198,14 @@ export default class HaAutomationOptionRow extends LitElement {
)
)}
</ha-dropdown-item>
<ha-dropdown-item value="edit_comment">
<ha-dropdown-item value="edit_note">
<ha-svg-icon
slot="icon"
.path=${mdiCommentEditOutline}
></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.option?.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.option?.note ? "edit" : "add"}`
)
)}
</ha-dropdown-item>
@@ -394,8 +393,8 @@ export default class HaAutomationOptionRow extends LitElement {
case "rename":
this._renameOption();
break;
case "edit_comment":
this._editCommentOption();
case "edit_note":
this._editNoteOption();
break;
case "delete":
this._removeOption();
@@ -460,28 +459,28 @@ export default class HaAutomationOptionRow extends LitElement {
}
};
private _editCommentOption = async (): Promise<void> => {
private _editNoteOption = async (): Promise<void> => {
if (!this.option) {
return;
}
const comment = await showPromptDialog(this, {
const note = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.option.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.option.note ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
),
inputType: "string",
defaultValue: this.option.comment,
defaultValue: this.option.note,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
if (note !== null) {
const value: Option = { ...this.option };
if (comment === "") {
delete value.comment;
if (note === "") {
delete value.note;
} else {
value.comment = comment;
value.note = note;
}
fireEvent(this, "value-changed", {
value,
@@ -537,11 +536,11 @@ export default class HaAutomationOptionRow extends LitElement {
rename: () => {
this._renameOption();
},
editComment: this._editCommentOption,
editNote: this._editNoteOption,
delete: this._removeOption,
duplicate: this._duplicateOption,
defaultOption: !!this.defaultActions,
comment: sidebarOption?.comment,
note: sidebarOption?.note,
} satisfies OptionSidebarConfig);
this._selected = true;
this._collapsed = false;
@@ -39,7 +39,7 @@ import { isMac } from "../../../../util/is_mac";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import { getAutomationActionType } from "../action/ha-automation-action-row";
import { getRepeatType } from "../action/types/ha-automation-action-repeat";
import "../ha-automation-comment";
import "../ha-automation-note";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
@@ -177,11 +177,11 @@ export default class HaAutomationSidebarAction extends LitElement {
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>
<ha-dropdown-item slot="menu-items" value="edit_comment">
<ha-dropdown-item slot="menu-items" value="edit_note">
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.config.config.action.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.config.config.action.note ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
@@ -388,11 +388,11 @@ export default class HaAutomationSidebarAction extends LitElement {
@ui-mode-not-available=${this._handleUiModeNotAvailable}
></ha-automation-action-editor>`
)}
${this.config.config.action.comment?.trim() && !this.yamlMode
? html`<ha-automation-comment
@edit-comment=${this.config.editComment}
.comment=${this.config.config.action.comment}
></ha-automation-comment>`
${this.config.config.action.note?.trim() && !this.yamlMode
? html`<ha-automation-note
@edit-note=${this.config.editNote}
.note=${this.config.config.action.note}
></ha-automation-note>`
: nothing}
</ha-automation-sidebar-card>`;
}
@@ -442,8 +442,8 @@ export default class HaAutomationSidebarAction extends LitElement {
case "rename":
this.config.rename();
break;
case "edit_comment":
this.config.editComment();
case "edit_note":
this.config.editNote();
break;
case "run":
this.config.run();
@@ -35,7 +35,7 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../condition/ha-automation-condition-editor";
import type HaAutomationConditionEditor from "../condition/ha-automation-condition-editor";
import "../ha-automation-comment";
import "../ha-automation-note";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
@@ -153,13 +153,13 @@ export default class HaAutomationSidebarCondition extends LitElement {
</ha-dropdown-item>
<ha-dropdown-item
slot="menu-items"
value="edit_comment"
value="edit_note"
.disabled=${this.disabled}
>
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.config.config.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.config.config.note ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
@@ -347,11 +347,11 @@ export default class HaAutomationSidebarCondition extends LitElement {
sidebar
></ha-automation-condition-editor>`
)}
${this.config.config.comment?.trim() && !this.yamlMode
? html`<ha-automation-comment
@edit-comment=${this.config.editComment}
.comment=${this.config.config.comment}
></ha-automation-comment>`
${this.config.config.note?.trim() && !this.yamlMode
? html`<ha-automation-note
@edit-note=${this.config.editNote}
.note=${this.config.config.note}
></ha-automation-note>`
: nothing}
<div class="testing-wrapper">
<div
@@ -417,8 +417,8 @@ export default class HaAutomationSidebarCondition extends LitElement {
case "rename":
this.config.rename();
break;
case "edit_comment":
this.config.editComment();
case "edit_note":
this.config.editNote();
break;
case "test":
this.config.test();
@@ -9,16 +9,16 @@ import {
import { html, LitElement, nothing } from "lit";
import { customElement, property, query } from "lit/decorators";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-svg-icon";
import type { OptionSidebarConfig } from "../../../../data/automation";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import "../ha-automation-comment";
import "../ha-automation-note";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
@customElement("ha-automation-sidebar-option")
export default class HaAutomationSidebarOption extends LitElement {
@@ -76,7 +76,7 @@ export default class HaAutomationSidebarOption extends LitElement {
</ha-dropdown-item>
<ha-dropdown-item
slot="menu-items"
value="edit_comment"
value="edit_note"
.disabled=${!!disabled}
>
<ha-svg-icon
@@ -85,7 +85,7 @@ export default class HaAutomationSidebarOption extends LitElement {
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.config.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.config.note ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
@@ -144,11 +144,11 @@ export default class HaAutomationSidebarOption extends LitElement {
`}
<div class="description">${description}</div>
${!this.config.defaultOption && this.config.comment?.trim()
? html`<ha-automation-comment
@edit-comment=${this.config.editComment}
.comment=${this.config.comment}
></ha-automation-comment>`
${!this.config.defaultOption && this.config.note?.trim()
? html`<ha-automation-note
@edit-note=${this.config.editNote}
.note=${this.config.note}
></ha-automation-note>`
: nothing}
</ha-automation-sidebar-card>`;
}
@@ -164,8 +164,8 @@ export default class HaAutomationSidebarOption extends LitElement {
case "rename":
this.config.rename();
break;
case "edit_comment":
this.config.editComment();
case "edit_note":
this.config.editNote();
break;
case "duplicate":
this.config.duplicate();
@@ -16,7 +16,7 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../../script/ha-script-field-editor";
import type HaAutomationConditionEditor from "../action/ha-automation-action-editor";
import "../ha-automation-comment";
import "../ha-automation-note";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "./ha-automation-sidebar-card";
@@ -68,11 +68,11 @@ export default class HaAutomationSidebarScriptField extends LitElement {
@wa-select=${this._handleDropdownSelect}
>
<span slot="title">${title}</span>
<ha-dropdown-item slot="menu-items" value="edit_comment">
<ha-dropdown-item slot="menu-items" value="edit_note">
<ha-svg-icon slot="icon" .path=${mdiCommentEditOutline}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.config.config.field.description ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.config.config.field.description ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
@@ -137,10 +137,10 @@ export default class HaAutomationSidebarScriptField extends LitElement {
></ha-script-field-editor>`
)}
${this.config.config.field.description?.trim() && !this.yamlMode
? html`<ha-automation-comment
@edit-comment=${this.config.editComment}
.comment=${this.config.config.field.description}
></ha-automation-comment>`
? html`<ha-automation-note
@edit-note=${this.config.editNote}
.note=${this.config.config.field.description}
></ha-automation-note>`
: nothing}
</ha-automation-sidebar-card>`;
}
@@ -189,8 +189,8 @@ export default class HaAutomationSidebarScriptField extends LitElement {
case "toggle_yaml_mode":
this._toggleYamlMode();
break;
case "edit_comment":
this.config.editComment();
case "edit_note":
this.config.editNote();
break;
case "delete":
this.config.delete();
@@ -34,7 +34,7 @@ import {
} from "../../../../data/trigger";
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../ha-automation-comment";
import "../ha-automation-note";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
@@ -132,7 +132,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
${type !== "list"
? html`<ha-dropdown-item
slot="menu-items"
value="edit_comment"
value="edit_note"
.disabled=${this.disabled}
>
<ha-svg-icon
@@ -141,7 +141,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.comment.${(this.config.config as Exclude<Trigger, TriggerList>).comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${(this.config.config as Exclude<Trigger, TriggerList>).note ? "edit" : "add"}`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
@@ -343,12 +343,12 @@ export default class HaAutomationSidebarTrigger extends LitElement {
></ha-automation-trigger-editor>`
)}
${!isTriggerList(this.config.config) &&
this.config.config.comment?.trim() &&
this.config.config.note?.trim() &&
!this.yamlMode
? html`<ha-automation-comment
@edit-comment=${this.config.editComment}
.comment=${this.config.config.comment}
></ha-automation-comment>`
? html`<ha-automation-note
@edit-note=${this.config.editNote}
.note=${this.config.config.note}
></ha-automation-note>`
: nothing}
</ha-automation-sidebar-card>
`;
@@ -401,8 +401,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
case "rename":
this.config.rename();
break;
case "edit_comment":
this.config.editComment();
case "edit_note":
this.config.editNote();
break;
case "show_id":
this._showTriggerId();
+1 -1
View File
@@ -4,7 +4,7 @@ export const baseTriggerStruct = object({
trigger: string(),
id: optional(string()),
enabled: optional(boolean()),
comment: optional(string()),
note: optional(string()),
});
export const forDictStruct = object({
+8 -3
View File
@@ -53,14 +53,19 @@ export const rowStyles = css`
position: absolute;
}
.comment-indicator {
.icon-badge-wrapper {
position: relative;
display: inline-flex;
}
.note-indicator {
color: var(--ha-color-on-neutral-normal);
}
.comment-indicator + ha-tooltip::part(body) {
.note-indicator + ha-tooltip::part(body) {
cursor: default;
max-width: 300px;
}
.comment-indicator + ha-tooltip p {
.note-indicator + ha-tooltip p {
white-space: pre-wrap;
margin: 0;
}
@@ -60,6 +60,9 @@ export class HaAutomationRowTargets extends LitElement {
@property({ attribute: false })
public selector?: TargetSelector;
@property({ type: Boolean })
public interactive = false;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@@ -89,7 +92,12 @@ export class HaAutomationRowTargets extends LitElement {
@consume({ context: statesContext, subscribe: true })
private _states!: ContextType<typeof statesContext>;
private _countCache = new Map<string, Promise<number | undefined>>();
private _countCache = new Map<
string,
Promise<number | undefined> | number | undefined
>();
private _rerenderCount = true;
protected willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
@@ -98,10 +106,15 @@ export class HaAutomationRowTargets extends LitElement {
changedProps.has("selector") ||
changedProps.has("_registries")
) {
this._countCache.clear();
this._rerenderCount = true;
}
}
protected updated(changedProps: PropertyValues) {
super.updated(changedProps);
this._rerenderCount = false;
}
private _countMatchingEntities(referencedEntities: string[]): number {
const targetSelector = this.selector;
const hasEntityFilter = !!targetSelector?.target?.entity;
@@ -148,7 +161,11 @@ export class HaAutomationRowTargets extends LitElement {
targetId: string
) {
const key = `${targetType}:${targetId}`;
if (!this._countCache.has(key)) {
let fallback = " (-)";
if (!this._countCache.has(key) || this._rerenderCount) {
if (typeof this._countCache.get(key) === "number") {
fallback = ` (${this._countCache.get(key)})`;
}
this._countCache.set(
key,
extractFromTarget(
@@ -162,15 +179,30 @@ export class HaAutomationRowTargets extends LitElement {
.then((result) =>
this._countMatchingEntities(result.referenced_entities)
)
.catch(() => undefined)
.catch((err) => {
// eslint-disable-next-line no-console
console.error("Error counting target entities", err);
return undefined;
})
);
}
return until(
this._countCache
.get(key)!
.then((count) => (count === undefined ? nothing : html` (${count})`)),
"(-)"
);
if (this._countCache.get(key) instanceof Promise) {
return until(
(this._countCache.get(key) as Promise<number | undefined>)!.then(
(count) => {
this._countCache.set(key, count);
return count === undefined ? nothing : html` (${count})`;
}
),
fallback
);
}
if (typeof this._countCache.get(key) === "number") {
return ` (${this._countCache.get(key)})`;
}
return nothing;
}
protected render() {
@@ -249,8 +281,9 @@ export class HaAutomationRowTargets extends LitElement {
<ha-dropdown
@wa-select=${this._handleTargetSelect}
@click=${stopPropagation}
@keydown=${stopPropagation}
>
<span slot="trigger" class="target interactive">
<button slot="trigger" class="target">
<ha-svg-icon .path=${mdiFormatListBulleted}></ha-svg-icon>
<div class="label">
${this._i18n.localize(
@@ -261,7 +294,7 @@ export class HaAutomationRowTargets extends LitElement {
)}
</div>
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
</span>
</button>
${rows.map(([targetType, targetId]) => {
const content = html`${lastTargetType !== null &&
lastTargetType !== targetType
@@ -316,21 +349,37 @@ export class HaAutomationRowTargets extends LitElement {
targetType?: string,
countTemplate: unknown = nothing
) {
return html`<div
if (!this.interactive || !targetId || !targetType) {
return html`<div
class=${classMap({
target: true,
warning,
error,
})}
.targetId=${targetId}
.targetType=${targetType}
.label=${label}
>
${icon}
<div class="label">${label}${countTemplate}</div>
</div>`;
}
return html`<button
class=${classMap({
target: true,
warning,
error,
interactive: targetId && targetType,
})}
.targetId=${targetId}
.targetType=${targetType}
.label=${label}
@click=${this._handleTargetClick}
@keydown=${this._handleTargetKeydown}
>
${icon}
<div class="label">${label}${countTemplate}</div>
</div>`;
</button>`;
}
private _renderTarget(
@@ -384,7 +433,7 @@ export class HaAutomationRowTargets extends LitElement {
targetId,
this._getLabel
);
if (targetType !== "entity") {
if (targetType !== "entity" && this.interactive) {
countTemplate = this._renderCount(targetType, targetId);
}
}
@@ -444,6 +493,13 @@ export class HaAutomationRowTargets extends LitElement {
this._showTargetInfo(target.targetId, target.targetType, target.label, ev);
}
private _handleTargetKeydown(ev: KeyboardEvent) {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault();
this._handleTargetClick(ev);
}
}
private _handleTargetSelect(
ev: HaDropdownSelectEvent<{
targetId?: string;
@@ -533,10 +589,10 @@ export class HaAutomationRowTargets extends LitElement {
align-items: center;
}
.target.interactive {
button.target {
cursor: pointer;
}
.target.interactive:hover {
button.target:hover {
background: var(--ha-color-fill-neutral-normal-hover);
}
@@ -141,7 +141,7 @@ export default class HaAutomationTriggerEditor extends LitElement {
ev.stopPropagation();
const value = {
...(this.trigger.alias ? { alias: this.trigger.alias } : {}),
...(this.trigger.comment ? { comment: this.trigger.comment } : {}),
...(this.trigger.note ? { note: this.trigger.note } : {}),
...ev.detail.value,
};
fireEvent(this, "value-changed", { value });
@@ -1,7 +1,6 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import {
mdiAlert,
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowUp,
@@ -29,7 +28,6 @@ import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../../common/array/ensure-array";
import { storage } from "../../../../common/decorators/storage";
import { transform } from "../../../../common/decorators/transform";
import { fireEvent } from "../../../../common/dom/fire_event";
import { preventDefaultStopPropagation } from "../../../../common/dom/prevent_default_stop_propagation";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
@@ -50,21 +48,15 @@ import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
import type {
AutomationClipboard,
AutomationConfig,
PlatformTrigger,
Trigger,
TriggerList,
TriggerSidebarConfig,
} from "../../../../data/automation";
import {
automationConfigContext,
isTrigger,
subscribeTrigger,
} from "../../../../data/automation";
import { isTrigger, subscribeTrigger } from "../../../../data/automation";
import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
@@ -81,7 +73,6 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../ha-automation-editor-warning";
import "../ha-trigger-id-chip";
import { overflowStyles, rowStyles } from "../styles";
import "../target/ha-automation-row-targets";
import "./ha-automation-trigger-editor";
@@ -187,30 +178,6 @@ export default class HaAutomationTriggerRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg: EntityRegistryEntry[] = [];
@state()
@consume({ context: automationConfigContext, subscribe: true })
@transform<AutomationConfig, boolean>({
transformer: function (this: HaAutomationTriggerRow, value) {
if (
!this.trigger ||
isTriggerList(this.trigger) ||
!(this.trigger as Exclude<Trigger, TriggerList>).id
) {
return false;
}
const triggerId = (this.trigger as Exclude<Trigger, TriggerList>).id;
// count how often this trigger id is used in the automation, if more than once, show warning
return (
ensureArray(value?.triggers || []).filter(
(trigger) =>
(trigger as Exclude<Trigger, TriggerList>).id === triggerId
).length > 1
);
},
watch: ["trigger"],
})
private _duplicateTriggerId = false;
get selected() {
return this._selected;
}
@@ -257,9 +224,9 @@ export default class HaAutomationTriggerRow extends LitElement {
?.target
: undefined;
const commentTooltipText = truncateWithEllipsis(
const noteTooltipText = truncateWithEllipsis(
(type !== "list" &&
(this.trigger as Exclude<Trigger, TriggerList>).comment?.trim()) ||
(this.trigger as Exclude<Trigger, TriggerList>).note?.trim()) ||
"",
250
);
@@ -277,50 +244,28 @@ export default class HaAutomationTriggerRow extends LitElement {
.trigger=${(this.trigger as Exclude<Trigger, TriggerList>).trigger}
></ha-trigger-icon>`}
<h3 slot="header">
${type !== "list" && (this.trigger as Exclude<Trigger, TriggerList>).id
? html`<ha-trigger-id-chip
id="trigger-id-chip"
.warning=${this._duplicateTriggerId}
slot="leading-icon"
.triggerId=${(this.trigger as Exclude<Trigger, TriggerList>).id}
>
${this._duplicateTriggerId
? html`<ha-svg-icon
slot="start"
.path=${mdiAlert}
></ha-svg-icon>`
: nothing}
</ha-trigger-id-chip>
${this._duplicateTriggerId
? html`<ha-tooltip for="trigger-id-chip">
${this.hass.localize(
"ui.panel.config.automation.editor.triggers.duplicate_id_warning"
)}
</ha-tooltip>`
: nothing} `
: nothing}
${describeTrigger(this.trigger, this.hass, this._entityReg)}
${target !== undefined || (descriptionHasTarget && !this._isNew)
? this._renderTargets(
target,
descriptionHasTarget && !this._isNew,
triggerTargetSpec
triggerTargetSpec,
type !== "device"
)
: nothing}
${type !== "list" &&
(this.trigger as Exclude<Trigger, TriggerList>).comment?.trim()
(this.trigger as Exclude<Trigger, TriggerList>).note?.trim()
? html`
<ha-svg-icon
id="comment-icon"
tabindex="0"
id="note-icon"
.path=${mdiCommentTextOutline}
.label=${this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
)}
class="comment-indicator"
class="note-indicator"
></ha-svg-icon>
<ha-tooltip for="comment-icon"
><p>${commentTooltipText}</p></ha-tooltip
>
<ha-tooltip for="note-icon"><p>${noteTooltipText}</p></ha-tooltip>
`
: nothing}
</h3>
@@ -364,14 +309,14 @@ export default class HaAutomationTriggerRow extends LitElement {
)}
</ha-dropdown-item>
${type !== "list"
? html`<ha-dropdown-item value="edit_comment">
? html`<ha-dropdown-item value="edit_note">
<ha-svg-icon
slot="icon"
.path=${mdiCommentEditOutline}
></ha-svg-icon>
${this._renderOverflowLabel(
this.hass.localize(
`ui.panel.config.automation.editor.comment.${(this.trigger as Exclude<Trigger, TriggerList>).comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${(this.trigger as Exclude<Trigger, TriggerList>).note ? "edit" : "add"}`
)
)}
</ha-dropdown-item>`
@@ -614,13 +559,14 @@ export default class HaAutomationTriggerRow extends LitElement {
(
target?: HassServiceTarget,
targetRequired = false,
targetSpec?: TargetSelector["target"]
targetSpec?: TargetSelector["target"],
interactive = false
) =>
html`<ha-automation-row-targets
.hass=${this.hass}
.target=${target}
.targetRequired=${targetRequired}
.selector=${targetSpec ? { target: targetSpec } : undefined}
.interactive=${interactive}
></ha-automation-row-targets>`
);
@@ -752,7 +698,7 @@ export default class HaAutomationTriggerRow extends LitElement {
rename: () => {
this._renameTrigger();
},
editComment: this._editCommentTrigger,
editNote: this._editNoteTrigger,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -896,27 +842,27 @@ export default class HaAutomationTriggerRow extends LitElement {
}
};
private _editCommentTrigger = async (): Promise<void> => {
private _editNoteTrigger = async (): Promise<void> => {
if (isTriggerList(this.trigger)) return;
const trigger = this.trigger;
const comment = await showPromptDialog(this, {
const note = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${trigger.comment ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${trigger.note ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
),
inputType: "string",
defaultValue: trigger.comment,
defaultValue: trigger.note,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
if (note !== null) {
const value = { ...trigger };
if (comment === "") {
delete value.comment;
if (note === "") {
delete value.note;
} else {
value.comment = comment;
value.note = note;
}
fireEvent(this, "value-changed", {
value,
@@ -1041,8 +987,8 @@ export default class HaAutomationTriggerRow extends LitElement {
case "rename":
this._renameTrigger();
break;
case "edit_comment":
this._editCommentTrigger();
case "edit_note":
this._editNoteTrigger();
break;
case "duplicate":
this._duplicateTrigger();
@@ -8,7 +8,6 @@ import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../common/array/ensure-array";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-button";
@@ -22,20 +21,15 @@ import {
} from "../../../../data/automation";
import { subscribeLabFeature } from "../../../../data/labs";
import type { TriggerDescriptions } from "../../../../data/trigger";
import {
getNextNumericTriggerId,
getUniqueTriggerId,
isTriggerList,
subscribeTriggers,
} from "../../../../data/trigger";
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { EDITOR_SAVE_FAB_TOAST_BOTTOM_OFFSET } from "../editor-toast";
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
import {
getAddAutomationElementTargetFromQuery,
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
import { automationRowsStyles } from "../styles";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
@@ -73,53 +67,6 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
this.highlightedTriggers = items;
}
protected override pasteItem(ev: CustomEvent) {
if (this.root && ev.detail.item) {
const pasted = deepClone(ev.detail.item) as Trigger;
if (!isTriggerList(pasted)) {
pasted.id = pasted.id
? getUniqueTriggerId(pasted.id, this.triggers)
: getNextNumericTriggerId(this.triggers);
}
ev.detail.item = pasted;
}
super.pasteItem(ev);
}
protected override insertAfter(ev: CustomEvent) {
// Only dedupe when a single trigger is being inserted.
const incoming = ensureArray(ev.detail.value) as Trigger[];
if (this.root && incoming.length === 1) {
const trigger = deepClone(incoming[0]);
if (!isTriggerList(trigger)) {
trigger.id = trigger.id
? getUniqueTriggerId(trigger.id, this.triggers)
: getNextNumericTriggerId(this.triggers);
}
ev.detail.value = trigger;
}
super.insertAfter(ev);
}
protected override duplicateItem(ev: CustomEvent) {
if (this.root) {
const index = (ev.target as any).index;
const duplicated = deepClone(this.triggers[index]);
if (!isTriggerList(duplicated)) {
duplicated.id = duplicated.id
? getUniqueTriggerId(duplicated.id, this.triggers)
: getNextNumericTriggerId(this.triggers);
}
fireEvent(this, "value-changed", {
// @ts-expect-error Requires library bump to ES2023
value: this.triggers.toSpliced(index + 1, 0, duplicated),
});
ev.stopPropagation();
return;
}
super.duplicateItem(ev);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribe();
@@ -266,36 +213,23 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
private _addTrigger = (value: string, target?: HassServiceTarget) => {
let triggers: Trigger[];
if (value === PASTE_VALUE) {
const pasted = deepClone(this._clipboard!.trigger!);
if (this.root && !isTriggerList(pasted)) {
pasted.id = pasted.id
? getUniqueTriggerId(pasted.id, this.triggers)
: getNextNumericTriggerId(this.triggers);
}
triggers = this.triggers.concat(pasted);
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger!));
} else if (isDynamic(value)) {
triggers = this.triggers.concat({
trigger: getValueFromDynamic(value),
target,
});
} else {
let newTrigger: Trigger;
if (isDynamic(value)) {
newTrigger = {
trigger: getValueFromDynamic(value),
target,
};
} else {
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
const elClass = customElements.get(
`ha-automation-trigger-${trigger}`
) as CustomElementConstructor & {
defaultConfig: Trigger;
};
newTrigger = {
...elClass.defaultConfig,
...(target?.entity_id ? { entity_id: target.entity_id } : {}),
};
}
if (this.root && !isTriggerList(newTrigger)) {
newTrigger.id = getNextNumericTriggerId(this.triggers);
}
triggers = this.triggers.concat(newTrigger);
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
const elClass = customElements.get(
`ha-automation-trigger-${trigger}`
) as CustomElementConstructor & {
defaultConfig: Trigger;
};
triggers = this.triggers.concat({
...elClass.defaultConfig,
...(target?.entity_id ? { entity_id: target.entity_id } : {}),
});
}
this.focusLastItemOnChange = true;
fireEvent(this, "value-changed", { value: triggers });
@@ -29,7 +29,7 @@ const DEFAULT_KEYS: (keyof PlatformTrigger)[] = [
"trigger",
"target",
"alias",
"comment",
"note",
"id",
"variables",
"enabled",
@@ -1,6 +1,6 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import { mdiCog, mdiContentCopy } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -13,10 +13,9 @@ import "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-icon-button";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import {
automationConfigContext,
type AutomationConfig,
type WebhookTrigger,
import type {
AutomationConfig,
WebhookTrigger,
} from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import { showEditorToast } from "../../editor-toast";
@@ -34,9 +33,9 @@ export class HaWebhookTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false;
@consume({ context: automationConfigContext, subscribe: true })
@state()
private _config?: AutomationConfig;
@state() private _config?: AutomationConfig;
private _unsub?: UnsubscribeFunc;
public static get defaultConfig(): WebhookTrigger {
return {
@@ -47,6 +46,24 @@ export class HaWebhookTrigger extends LitElement {
};
}
connectedCallback() {
super.connectedCallback();
const details = {
callback: (config) => {
this._config = config;
},
};
fireEvent(this, "subscribe-automation-config", details);
this._unsub = (details as any).unsub;
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._unsub) {
this._unsub();
}
}
private _generateWebhookId(): string {
// The webhook_id should be treated like a password. Generate a default
// value that would be hard for someone to guess. This generates a
@@ -22,8 +22,6 @@ class HaConfigBlueprint extends HassRouterPage {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
@property({ attribute: false })
public blueprints: Record<string, Blueprints> = {};
@@ -61,7 +59,6 @@ class HaConfigBlueprint extends HassRouterPage {
pageEl.narrow = this.narrow;
pageEl.isWide = this.isWide;
pageEl.route = this.routeTail;
pageEl.showAdvanced = this.showAdvanced;
pageEl.blueprints = this.blueprints;
if (
@@ -167,14 +167,15 @@ export class DialogTryTts extends LitElement {
}
this._message = message;
if (this._target === "browser") {
const target = this._target || "browser";
if (target === "browser") {
// We create the audio element here + do a play, because iOS requires it to be done by user action
const audio = new Audio();
audio.play();
this._playBrowser(message, audio);
} else {
this.hass.callService("tts", "cloud_say", {
entity_id: this._target,
entity_id: target,
message,
});
}
@@ -211,7 +211,6 @@ class HaConfigSectionGeneral extends LitElement {
<div class="unit-system-options">
<ha-select-box
name="unit_system"
.hass=${this.hass}
.value=${this._unitSystem}
.disabled=${disabled}
@value-changed=${this._unitSystemChanged}
@@ -42,8 +42,6 @@ class HaConfigSystemNavigation extends LitElement {
@property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ attribute: false }) public showAdvanced = false;
@state() private _latestBackupDate?: Date;
@state() private _boardName?: string;
@@ -299,9 +299,10 @@ class DialogNewDashboard extends LitElement implements HassDialog {
const options: IFuseOptions<CustomStrategyEntry> = {
keys: ["type", "name", "description"],
isCaseSensitive: false,
threshold: 0.3,
ignoreLocation: true,
minMatchCharLength: Math.min(filter.length, 2),
threshold: 0.3,
ignoreDiacritics: true,
ignoreLocation: true,
};
const fuse = new Fuse(strategies, options);
return fuse.search(filter).map((result) => result.item);
@@ -35,8 +35,6 @@ export class DeveloperYamlConfig extends LitElement {
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public showAdvanced = false;
@state() private _validating = false;
@state() private _reloadableDomains: TranslatedReloadableDomain[] = [];
@@ -12,8 +12,6 @@ import {
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-adaptive-dialog";
import "../../../../components/ha-list";
import "../../../../components/ha-list-item";
import "../../../../components/ha-spinner";
import type { AutomationConfig } from "../../../../data/automation";
import { showAutomationEditor } from "../../../../data/automation";
@@ -35,15 +33,38 @@ import {
} from "../../../../data/device/device_automation";
import type { ScriptConfig } from "../../../../data/script";
import { showScriptEditor } from "../../../../data/script";
import type { SceneEntities } from "../../../../data/scene";
import { showSceneEditor } from "../../../../data/scene";
import "../../../../dialogs/add-to/ha-add-to-action-list";
import type {
AddToActionListActionSelectedEvent,
AddToActionListItem,
AddToActionListSection,
} from "../../../../dialogs/add-to/ha-add-to-action-list";
import {
addToActionHandler,
type AddToActionKey,
} from "../../../../dialogs/more-info/add-to";
createAddToSceneEntities,
type AddToAutomationScriptActionKey,
} from "../../../../dialogs/add-to/add-to";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { DeviceAddToDialogParams } from "./show-dialog-device-add-to";
type DeviceLegacyAddToActionType =
| "trigger"
| "condition"
| "automation_action"
| "script_action";
type DeviceAddToAction =
| (AddToActionListItem & {
kind: "add-to";
key: AddToAutomationScriptActionKey;
})
| (AddToActionListItem & {
kind: "legacy";
legacyType: DeviceLegacyAddToActionType;
})
| (AddToActionListItem & { kind: "scene" });
@customElement("dialog-device-add-to")
export class DialogDeviceAddTo extends LitElement {
@state()
@@ -132,11 +153,18 @@ export class DialogDeviceAddTo extends LitElement {
return nothing;
}
const deviceName = computeDeviceNameDisplay(
this._params.device,
this._i18n.localize,
this._states
);
return html`
<ha-adaptive-dialog
.open=${this._open}
header-title=${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.title"
"ui.dialogs.more_info_control.add_to.title",
{ target: deviceName }
)}
@closed=${this._dialogClosed}
>
@@ -151,80 +179,62 @@ export class DialogDeviceAddTo extends LitElement {
if (!this._params) {
return nothing;
}
const deviceName = computeDeviceNameDisplay(
this._params.device,
this._i18n.localize,
this._states
);
const sections: AddToActionListSection<DeviceAddToAction>[] = [
{
title: this._i18n.localize(
"ui.panel.config.devices.automation.automations_heading"
),
actions: [
{
kind: "add-to",
key: "automation_trigger",
iconPath: mdiRobotOutline,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
),
},
{
kind: "add-to",
key: "automation_condition",
iconPath: mdiPlaylistCheck,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
),
},
{
kind: "add-to",
key: "automation_action",
iconPath: mdiPlayCircleOutline,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
),
},
],
},
{
title: this._i18n.localize(
"ui.panel.config.devices.script.scripts_heading"
),
actions: [
{
kind: "add-to",
key: "script_action",
iconPath: mdiScriptTextOutline,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.script_action"
),
},
],
},
];
this._addSceneSection(sections);
return html`
<h3 class="section-header">
${this._i18n.localize(
"ui.panel.config.devices.automation.automations_heading"
)}
</h3>
<ha-list>
<ha-list-item
graphic="icon"
data-type="automation_trigger"
@click=${this._handleNewAction}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${mdiRobotOutline}></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
{ target: deviceName }
)}
</ha-list-item>
<ha-list-item
graphic="icon"
data-type="automation_condition"
@click=${this._handleNewAction}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${mdiPlaylistCheck}></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
{ target: deviceName }
)}
</ha-list-item>
<ha-list-item
graphic="icon"
data-type="automation_action"
@click=${this._handleNewAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_action",
{ target: deviceName }
)}
</ha-list-item>
</ha-list>
<h3 class="section-header">
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
</h3>
<ha-list>
<ha-list-item
graphic="icon"
data-type="script_action"
@click=${this._handleNewAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiScriptTextOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.script_action",
{ target: deviceName }
)}
</ha-list-item>
</ha-list>
${this._renderSceneSection(deviceName)}
<ha-add-to-action-list
.sections=${sections}
@add-to-list-action-selected=${this._handleActionSelected}
></ha-add-to-action-list>
`;
}
@@ -242,12 +252,6 @@ export class DialogDeviceAddTo extends LitElement {
return nothing;
}
const deviceName = computeDeviceNameDisplay(
this._params.device,
this._i18n.localize,
this._states
);
const hasTriggers = Boolean(this._triggers?.length);
const hasConditions = Boolean(this._conditions?.length);
const hasActions = Boolean(this._actions?.length);
@@ -263,165 +267,138 @@ export class DialogDeviceAddTo extends LitElement {
`;
}
return html`
<h3 class="section-header">
${this._i18n.localize(
"ui.panel.config.devices.automation.automations_heading"
)}
</h3>
${hasTriggers || hasConditions || hasActions
? html`
<ha-list>
${hasTriggers
? html`
<ha-list-item
graphic="icon"
data-type="trigger"
@click=${this._handleLegacyAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiRobotOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
{ target: deviceName }
)}
</ha-list-item>
`
: nothing}
${hasConditions
? html`
<ha-list-item
graphic="icon"
data-type="condition"
@click=${this._handleLegacyAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiPlaylistCheck}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
{ target: deviceName }
)}
</ha-list-item>
`
: nothing}
${hasActions
? html`
<ha-list-item
graphic="icon"
data-type="automation_action"
@click=${this._handleLegacyAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_action",
{ target: deviceName }
)}
</ha-list-item>
`
: nothing}
</ha-list>
`
: html`
<ha-list>
<ha-list-item noninteractive>
${this._i18n.localize(
"ui.panel.config.devices.automation.no_automations"
)}
</ha-list-item>
</ha-list>
`}
<h3 class="section-header">
${this._i18n.localize("ui.panel.config.devices.script.scripts_heading")}
</h3>
${hasActions
? html`
<ha-list>
<ha-list-item
graphic="icon"
data-type="script_action"
@click=${this._handleLegacyAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiScriptTextOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.script_action",
{ target: deviceName }
)}
</ha-list-item>
</ha-list>
`
: html`
<ha-list>
<ha-list-item noninteractive>
${this._i18n.localize(
"ui.panel.config.devices.script.no_scripts"
)}
</ha-list-item>
</ha-list>
`}
${this._renderSceneSection(deviceName)}
`;
}
private _renderSceneSection(deviceName: string) {
if (!this._params?.entityIds.length) {
return nothing;
const automationActions: DeviceAddToAction[] = [];
if (hasTriggers) {
automationActions.push({
kind: "legacy",
legacyType: "trigger",
iconPath: mdiRobotOutline,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.automation_trigger"
),
});
}
if (hasConditions) {
automationActions.push({
kind: "legacy",
legacyType: "condition",
iconPath: mdiPlaylistCheck,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.automation_condition"
),
});
}
if (hasActions) {
automationActions.push({
kind: "legacy",
legacyType: "automation_action",
iconPath: mdiPlayCircleOutline,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.automation_action"
),
});
}
const scriptActions: DeviceAddToAction[] = hasActions
? [
{
kind: "legacy",
legacyType: "script_action",
iconPath: mdiScriptTextOutline,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.script_action"
),
},
]
: [];
const sections: AddToActionListSection<DeviceAddToAction>[] = [
{
title: this._i18n.localize(
"ui.panel.config.devices.automation.automations_heading"
),
actions: automationActions,
empty: automationActions.length
? undefined
: this._i18n.localize(
"ui.panel.config.devices.automation.no_automations"
),
},
{
title: this._i18n.localize(
"ui.panel.config.devices.script.scripts_heading"
),
actions: scriptActions,
empty: scriptActions.length
? undefined
: this._i18n.localize("ui.panel.config.devices.script.no_scripts"),
},
];
this._addSceneSection(sections);
return html`
<h3 class="section-header">
${this._i18n.localize("ui.panel.config.devices.scene.scenes_heading")}
</h3>
<ha-list>
<ha-list-item
graphic="icon"
@click=${this._handleCreateScene}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.scene",
{ target: deviceName }
)}
</ha-list-item>
</ha-list>
<ha-add-to-action-list
.sections=${sections}
@add-to-list-action-selected=${this._handleActionSelected}
></ha-add-to-action-list>
`;
}
private _handleNewAction(ev: Event) {
private _addSceneSection(
sections: AddToActionListSection<DeviceAddToAction>[]
): void {
if (!this._params?.canCreateScene || !this._params.entityIds.length) {
return;
}
sections.push({
title: this._i18n.localize(
"ui.panel.config.devices.scene.scenes_heading"
),
actions: [
{
kind: "scene",
iconPath: mdiPalette,
name: this._i18n.localize(
"ui.dialogs.more_info_control.add_to.action_options.scene"
),
},
],
});
}
private _handleActionSelected(
ev: AddToActionListActionSelectedEvent<DeviceAddToAction>
) {
if (!this._params) {
return;
}
const key = (ev.currentTarget as HTMLElement).dataset
.type as AddToActionKey;
const { action } = ev.detail;
if (action.kind === "scene") {
this._handleCreateScene();
return;
}
if (action.kind === "add-to") {
this._handleAddToAction(action.key);
return;
}
this._handleLegacyAction(action.legacyType);
}
private _handleAddToAction(key: AddToAutomationScriptActionKey) {
if (!this._params) {
return;
}
this.closeDialog();
addToActionHandler(key, { device_id: this._params.device.id });
}
// When new_triggers_conditions labs feature is promoted, this whole method can be removed.
private _handleLegacyAction(ev: Event) {
if (!this._params) {
return;
}
const type = (ev.currentTarget as HTMLElement).dataset.type as
| "trigger"
| "condition"
| "automation_action"
| "script_action";
private _handleLegacyAction(type: DeviceLegacyAddToActionType) {
this.closeDialog();
if (type === "script_action") {
@@ -430,29 +407,28 @@ export class DialogDeviceAddTo extends LitElement {
newScript.sequence = [this._actions[0]];
}
showScriptEditor(newScript, true);
} else {
const newAutomation = {} as AutomationConfig;
if (type === "trigger" && this._triggers?.length) {
newAutomation.triggers = [this._triggers[0]];
} else if (type === "condition" && this._conditions?.length) {
newAutomation.conditions = [this._conditions[0]];
} else if (type === "automation_action" && this._actions?.length) {
newAutomation.actions = [this._actions[0]];
}
showAutomationEditor(newAutomation, true);
return;
}
const newAutomation = {} as AutomationConfig;
if (type === "trigger" && this._triggers?.length) {
newAutomation.triggers = [this._triggers[0]];
} else if (type === "condition" && this._conditions?.length) {
newAutomation.conditions = [this._conditions[0]];
} else if (type === "automation_action" && this._actions?.length) {
newAutomation.actions = [this._actions[0]];
}
showAutomationEditor(newAutomation, true);
}
private _handleCreateScene() {
if (!this._params) {
return;
}
const entities: SceneEntities = {};
for (const entityId of this._params.entityIds) {
entities[entityId] = "";
}
this.closeDialog();
showSceneEditor({ entities });
showSceneEditor({
entities: createAddToSceneEntities(this._params.entityIds),
});
}
static get styles(): CSSResultGroup {
@@ -469,14 +445,6 @@ export class DialogDeviceAddTo extends LitElement {
padding: var(--ha-space-4);
text-align: center;
}
.section-header {
padding: var(--ha-space-2) var(--ha-space-4) 0;
margin: 0;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
color: var(--secondary-text-color);
}
`,
];
}
@@ -5,6 +5,7 @@ export interface DeviceAddToDialogParams {
device: DeviceRegistryEntry;
newTriggersConditions: boolean;
entityIds: string[];
canCreateScene: boolean;
}
export const loadDeviceAddToDialog = () => import("./ha-device-add-to-dialog");

Some files were not shown because too many files have changed in this diff Show More