Compare commits

...

74 Commits

Author SHA1 Message Date
Wendelin de0a322c52 fix behavior img names 2026-05-28 08:27:17 +02:00
Simon Lamon f37cf1e848 Don't redispatch the original event in a checklist item (#52242) 2026-05-28 08:26:00 +03:00
Paulus Schoutsen a188ef1b7a Fix resend-verification flash and concurrency on cloud signup (#52244)
Resending the confirmation email reused the registration code path, so
the flash on the login screen said "Account created!" even though no
new account was created. Pass a message key to _verificationEmailSent
so resend can show "Verification email sent." instead.

_handleResendVerifyEmail also never set _requestInProgress, so the
resend button (and the start-trial button, which share that flag) were
not disabled while a resend was in flight and could be clicked
repeatedly. Set the flag at the start and clear it on terminal errors;
_verificationEmailSent already clears it on success.

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-28 08:11:54 +03:00
Wendelin 087ef159df Fix ha-radio-option checked theming (#52237)
Update ha-radio-option theming to use checked-icon-color for text and border
2026-05-27 15:58:00 +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
104 changed files with 1499 additions and 1295 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.0"
license = "Apache-2.0"
license-files = ["LICENSE*"]
description = "The Home Assistant frontend"
@@ -1,5 +1,6 @@
import { LitElement, css, html } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-tooltip";
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
@@ -12,6 +13,7 @@ export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
*
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
* @attr {string} label - Accessible label announced by assistive technology.
* @attr {string} message - Optional tooltip body shown on hover/focus.
*/
@customElement("ha-automation-row-live-test")
export class HaAutomationRowLiveTest extends LitElement {
@@ -19,6 +21,8 @@ export class HaAutomationRowLiveTest extends LitElement {
@property() public label = "";
@property() public message?: string;
protected render() {
return html`
<div
@@ -27,6 +31,9 @@ export class HaAutomationRowLiveTest extends LitElement {
tabindex="0"
aria-label=${this.label}
></div>
${this.message
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
: nothing}
`;
}
-18
View File
@@ -956,29 +956,11 @@ 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;
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") {
const minX = xAxis?.min
? xAxis.min instanceof Date
+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);
}
+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()}
`;
@@ -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"] {
+6 -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>;
}
@@ -491,12 +490,12 @@ export const migrateAutomationTrigger = (
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 +609,7 @@ export interface AutomationClipboard {
export interface BaseSidebarConfig {
delete: () => void;
close: (focus?: boolean) => void;
editComment: () => void;
editNote: () => void;
}
export interface TriggerSidebarConfig extends BaseSidebarConfig {
@@ -672,7 +671,7 @@ export interface OptionSidebarConfig extends BaseSidebarConfig {
rename: () => void;
duplicate: () => void;
defaultOption?: boolean;
comment?: string;
note?: string;
}
export interface ScriptFieldSidebarConfig extends BaseSidebarConfig {
@@ -698,7 +697,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
@@ -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<
@@ -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,
});
},
+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;
}
@@ -863,11 +867,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
@@ -145,8 +145,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[] = [];
@@ -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;
}
}
@@ -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
);
@@ -337,18 +337,18 @@ export default class HaAutomationActionRow extends LitElement {
serviceTargetSpec
)
: nothing}
${commentTooltipText
${noteTooltipText
? html`
<ha-svg-icon
id="comment-icon"
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}
@@ -407,11 +407,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>
@@ -941,25 +941,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 +1089,7 @@ export default class HaAutomationActionRow extends LitElement {
rename: () => {
this._renameAction();
},
editComment: this._editCommentAction,
editNote: this._editNoteAction,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -1185,8 +1185,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();
@@ -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";
@@ -53,26 +52,18 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
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 +83,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 +154,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,8 +205,8 @@ 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
);
@@ -228,13 +217,9 @@ export default class HaAutomationConditionRow extends LitElement {
.condition=${this.condition.condition}
></ha-condition-icon>
<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,
@@ -242,19 +227,17 @@ export default class HaAutomationConditionRow extends LitElement {
conditionTargetSpec
)
: nothing}
${this.condition.comment?.trim()
${this.condition.note?.trim()
? html`
<ha-svg-icon
id="comment-icon"
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>
@@ -304,11 +287,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>
@@ -549,11 +532,12 @@ export default class HaAutomationConditionRow extends LitElement {
<ha-automation-row-live-test
slot="icons"
.state=${this.condition.condition !== "trigger"
? this._liveTestResult
? this._liveTestResult.state
: "unknown"}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult : "unknown"}`
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult.state : "unknown"}`
)}
.message=${this._liveTestResult.message}
></ha-automation-row-live-test
></ha-automation-row>`
: html`
@@ -585,102 +569,6 @@ 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,
@@ -736,7 +624,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 +654,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 +676,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 +848,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 +1021,7 @@ export default class HaAutomationConditionRow extends LitElement {
rename: () => {
this._renameCondition();
},
editComment: this._editCommentCondition,
editNote: this._editNoteCondition,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -1190,8 +1093,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 +1127,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()),
@@ -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,17 @@ 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"
.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 +197,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 +392,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 +458,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 +535,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({
+3 -3
View File
@@ -53,14 +53,14 @@ export const rowStyles = css`
position: absolute;
}
.comment-indicator {
.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;
}
@@ -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,28 +244,6 @@ 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(
@@ -308,19 +253,17 @@ export default class HaAutomationTriggerRow extends LitElement {
)
: 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"
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 +307,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>`
@@ -752,7 +695,7 @@ export default class HaAutomationTriggerRow extends LitElement {
rename: () => {
this._renameTrigger();
},
editComment: this._editCommentTrigger,
editNote: this._editNoteTrigger,
toggleYamlMode: () => {
this._toggleYamlMode();
this.openSidebar();
@@ -896,27 +839,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 +984,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,
});
}
@@ -217,7 +217,7 @@ export class CloudRegister extends LitElement {
try {
await cloudRegister(this.hass, email, password);
this._verificationEmailSent(email);
this._verificationEmailSent(email, "account_created");
} catch (err: any) {
this._password = "";
this._requestInProgress = false;
@@ -238,15 +238,18 @@ export class CloudRegister extends LitElement {
const email = emailField.value || "";
this._requestInProgress = true;
const doResend = async (username: string) => {
try {
await cloudResendVerification(this.hass, username);
this._verificationEmailSent(username);
this._verificationEmailSent(username, "verification_email_sent");
} catch (err: any) {
const errCode = err && err.body && err.body.code;
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
await doResend(username.toLowerCase());
} else {
this._requestInProgress = false;
this._error =
err && err.body && err.body.message
? err.body.message
@@ -258,13 +261,16 @@ export class CloudRegister extends LitElement {
await doResend(email);
}
private _verificationEmailSent(email: string) {
private _verificationEmailSent(
email: string,
messageKey: "account_created" | "verification_email_sent"
) {
this._requestInProgress = false;
this._password = "";
fireEvent(this, "cloud-email-changed", { value: email });
fireEvent(this, "cloud-done", {
flashMessage: this.hass.localize(
"ui.panel.config.cloud.register.account_created"
`ui.panel.config.cloud.register.${messageKey}`
),
});
}
@@ -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;
@@ -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[] = [];
@@ -188,8 +188,6 @@ export class HaConfigDevicePage extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
@state() private _related?: RelatedResult;
@state() private _diagnosticDownloadLinks: DeviceAction[] = [];
@@ -18,8 +18,6 @@ class HaConfigDevices extends HassRouterPage {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
routes: {
@@ -53,7 +51,6 @@ class HaConfigDevices extends HassRouterPage {
pageEl.manifests = this._manifests;
pageEl.narrow = this.narrow;
pageEl.isWide = this.isWide;
pageEl.showAdvanced = this.showAdvanced;
pageEl.route = this.routeTail;
}
@@ -68,8 +68,6 @@ class HaConfigEnergy extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
@property({ attribute: false }) public route!: Route;
@state() private _searchParms = new URLSearchParams(window.location.search);
@@ -23,6 +23,7 @@ import "../../../components/ha-alert";
import "../../../components/ha-area-picker";
import "../../../components/ha-color-picker";
import "../../../components/ha-dropdown-item";
import "../../../components/entity/ha-entity-picker";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button-next";
import "../../../components/ha-icon-picker";
@@ -56,6 +57,7 @@ import { updateDeviceRegistryEntry } from "../../../data/device/device_registry"
import type {
AlarmControlPanelEntityOptions,
CalendarEntityOptions,
DeviceTrackerEntityOptions,
EntityRegistryEntry,
EntityRegistryEntryUpdateParams,
ExtEntityRegistryEntry,
@@ -97,7 +99,7 @@ import type { HomeAssistant } from "../../../types";
import { showToast } from "../../../util/toast";
import { showDeviceRegistryDetailDialog } from "../devices/device-registry-detail/show-dialog-device-registry-detail";
const OVERRIDE_DEVICE_CLASSES = {
export const OVERRIDE_DEVICE_CLASSES = {
cover: [
[
"awning",
@@ -138,6 +140,10 @@ const SWITCH_AS_DOMAINS_INVERT = ["cover", "lock", "valve"];
const PRECISIONS = [0, 1, 2, 3, 4, 5, 6];
const SCANNER_SOURCE_TYPES = ["router", "bluetooth", "bluetooth_le"];
const ZONE_DOMAINS = ["zone"];
@customElement("entity-registry-settings-editor")
export class EntityRegistrySettingsEditor extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -202,6 +208,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
@state() private _calendarColor?: string | null;
@state() private _associatedZone?: string;
@state() private _noDeviceArea?: boolean;
private _origEntityId!: string;
@@ -292,6 +300,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._calendarColor = this.entry.options?.calendar?.color;
}
if (domain === "device_tracker") {
this._associatedZone =
this.entry.options?.device_tracker?.associated_zone ?? "zone.home";
}
if (domain === "weather") {
const stateObj: HassEntity | undefined =
this.hass.states[this.entry.entity_id];
@@ -713,6 +726,21 @@ export class EntityRegistrySettingsEditor extends LitElement {
></ha-color-picker>
`
: nothing}
${domain === "device_tracker" &&
SCANNER_SOURCE_TYPES.includes(stateObj?.attributes?.source_type)
? html`
<ha-entity-picker
.hass=${this.hass}
.value=${this._associatedZone}
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.associated_zone"
)}
.includeDomains=${ZONE_DOMAINS}
.disabled=${this.disabled}
@value-changed=${this._associatedZoneChanged}
></ha-entity-picker>
`
: nothing}
${domain === "sensor" &&
this._deviceClass &&
stateObj?.attributes.unit_of_measurement &&
@@ -1208,6 +1236,17 @@ export class EntityRegistrySettingsEditor extends LitElement {
(params.options as CalendarEntityOptions).color = this._calendarColor;
}
}
if (
domain === "device_tracker" &&
this._associatedZone !== undefined &&
(this.entry.options?.device_tracker?.associated_zone ?? "zone.home") !==
this._associatedZone
) {
params.options_domain = "device_tracker";
params.options = {
associated_zone: this._associatedZone,
} as DeviceTrackerEntityOptions;
}
if (
domain === "weather" &&
(stateObj?.attributes?.precipitation_unit !== this._precipitation_unit ||
@@ -1447,6 +1486,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
this._calendarColor = ev.detail.value || null;
}
private _associatedZoneChanged(ev: CustomEvent): void {
fireEvent(this, "change");
this._associatedZone = ev.detail.value || "zone.home";
}
private _precipitationUnitChanged(ev: HaSelectSelectEvent): void {
fireEvent(this, "change");
this._precipitation_unit = ev.detail.value;
-1
View File
@@ -819,7 +819,6 @@ class HaPanelConfig extends HassRouterPage {
el.route = this.routeTail;
el.hass = this.hass;
el.showAdvanced = Boolean(this.hass.userData?.showAdvanced);
el.isWide = isWide;
el.narrow = this.narrow;
el.cloudStatus = this._cloudStatus;
@@ -1213,7 +1213,6 @@ ${rejected
showConfigFlowDialog(this, {
startFlowHandler: domain,
manifest: await fetchIntegrationManifest(this.hass, domain),
showAdvanced: this.hass.userData?.showAdvanced,
});
}
-2
View File
@@ -92,8 +92,6 @@ class HaConfigInfo extends LitElement {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
@property({ attribute: false }) public route!: Route;
@state() private _osInfo?: HassioHassOSInfo;
@@ -761,7 +761,6 @@ class AddIntegrationDialog extends LitElement {
showConfigFlowDialog(this, {
startFlowHandler: domain,
showAdvanced: this.hass.userData?.showAdvanced,
manifest,
navigateToResult: this._navigateToResult,
});
@@ -626,7 +626,6 @@ export class HaConfigEntryRow extends LitElement {
private _handleReconfigure = async () => {
showConfigFlowDialog(this, {
startFlowHandler: this.data.entry.domain,
showAdvanced: this.hass.userData?.showAdvanced,
manifest: await fetchIntegrationManifest(
this.hass,
this.data.entry.domain
@@ -138,8 +138,6 @@ class HaConfigIntegrationPage extends SubscribeMixin(LitElement) {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
@property({ attribute: false }) public configEntries?: ConfigEntry[];
@property({ attribute: false })
@@ -129,8 +129,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public configEntries?: ConfigEntryExtended[];
@@ -989,7 +987,6 @@ class HaConfigIntegrationsDashboard extends KeyboardShortcutMixin(
this.hass,
integration.supported_by!
),
showAdvanced: this.hass.userData?.showAdvanced,
});
},
});
@@ -51,8 +51,6 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
@property({ attribute: false }) public showAdvanced = false;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
routes: {
@@ -207,7 +205,6 @@ class HaConfigIntegrations extends SubscribeMixin(HassRouterPage) {
pageEl.configEntriesInProgress = this._configEntriesInProgress;
pageEl.narrow = this.narrow;
pageEl.isWide = this.isWide;
pageEl.showAdvanced = this.showAdvanced;
}
}
@@ -318,7 +318,6 @@ class HaDomainIntegrations extends LitElement {
root instanceof ShadowRoot ? (root.host as HTMLElement) : this,
{
startFlowHandler: domain,
showAdvanced: this.hass.userData?.showAdvanced,
navigateToResult: this.navigateToResult,
manifest: await fetchIntegrationManifest(this.hass, domain),
}
@@ -337,7 +336,6 @@ class HaDomainIntegrations extends LitElement {
{
continueFlowId: flow.flow_id,
navigateToResult: this.navigateToResult,
showAdvanced: this.hass.userData?.showAdvanced,
manifest: await fetchIntegrationManifest(this.hass, flow.handler),
}
);
@@ -3,16 +3,28 @@ import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { computeDomain } from "../../../../../common/entity/compute_domain";
import { computeDeviceName } from "../../../../../common/entity/compute_device_name";
import { navigate } from "../../../../../common/navigate";
import "../../../../../components/ha-dialog-footer";
import "../../../../../components/ha-icon-button-arrow-prev";
import "../../../../../components/ha-button";
import "../../../../../components/ha-dialog";
import {
commissionMatterDevice,
redirectOnNewMatterDevice,
watchForNewMatterDevice,
} from "../../../../../data/matter";
import { haStyleDialog } from "../../../../../resources/styles";
import type { HomeAssistant } from "../../../../../types";
import type { DeviceRegistryEntry } from "../../../../../data/device/device_registry";
import { updateDeviceRegistryEntry } from "../../../../../data/device/device_registry";
import {
getAutomaticEntityIds,
getExtendedEntityRegistryEntries,
updateEntityRegistryEntry,
type ExtEntityRegistryEntry,
} from "../../../../../data/entity/entity_registry";
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
import "./matter-add-device/matter-add-device-apple-home";
import "./matter-add-device/matter-add-device-existing";
import "./matter-add-device/matter-add-device-generic";
@@ -21,6 +33,7 @@ import "./matter-add-device/matter-add-device-google-home-fallback";
import "./matter-add-device/matter-add-device-main";
import "./matter-add-device/matter-add-device-new";
import "./matter-add-device/matter-add-device-commissioning";
import "./matter-add-device/matter-add-device-device-added";
import { showToast } from "../../../../../util/toast";
export type MatterAddDeviceStep =
@@ -31,7 +44,8 @@ export type MatterAddDeviceStep =
| "google_home_fallback"
| "apple_home"
| "generic"
| "commissioning";
| "commissioning"
| "device_added";
declare global {
interface HASSDomEvents {
@@ -50,6 +64,7 @@ const BACK_STEP: Record<MatterAddDeviceStep, MatterAddDeviceStep | undefined> =
apple_home: "existing",
generic: "existing",
commissioning: undefined,
device_added: undefined,
};
@customElement("dialog-matter-add-device")
@@ -62,23 +77,90 @@ class DialogMatterAddDevice extends LitElement {
@state() _step: MatterAddDeviceStep = "main";
@state() private _newDevice?: DeviceRegistryEntry;
@state() private _mainEntity?: ExtEntityRegistryEntry;
@state() private _deviceAddedState: {
name: string;
area: string | undefined;
deviceClass: string | undefined;
hasPendingUpdates: boolean;
} = {
name: "",
area: undefined,
deviceClass: undefined,
hasPendingUpdates: false,
};
private _mainEntityFetched = false;
private _unsub?: UnsubscribeFunc;
public showDialog(): void {
this._open = true;
this._unsub = redirectOnNewMatterDevice(this.hass, () =>
this.closeDialog()
);
this._unsub = watchForNewMatterDevice(this.hass, (device) => {
this._newDevice = device;
this._step = "device_added";
this._fetchMainEntity();
});
}
public closeDialog(): void {
this._open = false;
}
protected updated(changedProps: Map<string, unknown>): void {
// Retry fetching main entity when hass updates (entities may not be available immediately)
if (
changedProps.has("hass") &&
this._newDevice &&
!this._mainEntityFetched
) {
this._fetchMainEntity();
}
}
private async _fetchMainEntity(): Promise<void> {
if (!this._newDevice || this._mainEntityFetched) return;
const entityIds = Object.values(this.hass.entities)
.filter((e) => e.device_id === this._newDevice!.id)
.map((e) => e.entity_id);
if (!entityIds.length) return;
this._mainEntityFetched = true;
const entries = await getExtendedEntityRegistryEntries(
this.hass,
entityIds
);
const mainEntry = Object.values(entries).find(
(e) => e.original_name === null
);
if (!mainEntry) return;
const domain = computeDomain(mainEntry.entity_id);
if (domain === "cover" || domain === "binary_sensor") {
this._mainEntity = mainEntry;
}
}
private _dialogClosed(): void {
this._open = false;
this._step = "main";
this._pairingCode = "";
this._newDevice = undefined;
this._mainEntity = undefined;
this._mainEntityFetched = false;
this._deviceAddedState = {
name: "",
area: undefined,
deviceClass: undefined,
hasPendingUpdates: false,
};
this._unsub?.();
this._unsub = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
@@ -93,6 +175,17 @@ class DialogMatterAddDevice extends LitElement {
this._pairingCode = ev.detail.code;
}
private _handleDeviceAddedChanged(
ev: CustomEvent<{
name: string;
area: string | undefined;
deviceClass: string | undefined;
hasPendingUpdates: boolean;
}>
) {
this._deviceAddedState = ev.detail;
}
private _back() {
const backStep = BACK_STEP[this._step];
if (!backStep) return;
@@ -104,12 +197,15 @@ class DialogMatterAddDevice extends LitElement {
<div
@pairing-code-changed=${this._handlePairingCodeChanged}
@step-selected=${this._handleStepSelected}
@device-added-changed=${this._handleDeviceAddedChanged}
.hass=${this.hass}
>
${dynamicElement(
`matter-add-device-${this._step.replaceAll("_", "-")}`,
{
hass: this.hass,
device: this._newDevice,
mainEntity: this._mainEntity,
}
)}
</div>
@@ -129,8 +225,86 @@ class DialogMatterAddDevice extends LitElement {
),
duration: 2000,
});
this._step = savedStep;
}
this._step = savedStep;
// On success, keep showing commissioning spinner until watchForNewMatterDevice fires
}
private async _finishDeviceAdded(): Promise<void> {
const device = this._newDevice!;
const { name, area, deviceClass, hasPendingUpdates } =
this._deviceAddedState;
if (hasPendingUpdates) {
const origName = computeDeviceName(device) ?? "";
const nameChanged = name !== origName;
const origArea = device.area_id ?? undefined;
const areaChanged = area !== origArea;
if (nameChanged || areaChanged) {
await updateDeviceRegistryEntry(this.hass, device.id, {
...(nameChanged && { name_by_user: name || null }),
...(areaChanged && { area_id: area || null }),
}).catch((err: Error) =>
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_device",
{ error: err.message }
),
})
);
}
if (nameChanged && name) {
const entityIds = Object.values(this.hass.entities)
.filter((e) => e.device_id === device.id)
.map((e) => e.entity_id);
if (entityIds.length) {
const mapping = await getAutomaticEntityIds(this.hass, entityIds);
await Promise.allSettled(
Object.entries(mapping)
.filter((entry): entry is [string, string] => !!entry[1])
.map(([oldId, newId]) =>
updateEntityRegistryEntry(this.hass, oldId, {
new_entity_id: newId,
}).catch((err: Error) =>
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_entity",
{ error: err.message }
),
})
)
)
);
}
}
if (this._mainEntity) {
const origClass =
this._mainEntity.device_class ??
this._mainEntity.original_device_class ??
undefined;
if (deviceClass !== origClass) {
await updateEntityRegistryEntry(
this.hass,
this._mainEntity.entity_id,
{ device_class: deviceClass || null }
).catch((err: Error) =>
showAlertDialog(this, {
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.error_saving_entity",
{ error: err.message }
),
})
);
}
}
}
this.closeDialog();
navigate(`/config/devices/device/${device.id}`);
}
private _renderActions() {
@@ -156,6 +330,19 @@ class DialogMatterAddDevice extends LitElement {
</ha-button>
`;
}
if (this._step === "device_added") {
return html`
<ha-button slot="primaryAction" @click=${this._finishDeviceAdded}>
${this._deviceAddedState.hasPendingUpdates
? this.hass.localize(
"ui.dialogs.matter-add-device.device_added.finish"
)
: this.hass.localize(
"ui.dialogs.matter-add-device.device_added.skip"
)}
</ha-button>
`;
}
return nothing;
}
@@ -0,0 +1,283 @@
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { computeDomain } from "../../../../../../common/entity/compute_domain";
import { computeDeviceName } from "../../../../../../common/entity/compute_device_name";
import { caseInsensitiveStringCompare } from "../../../../../../common/string/compare";
import { fireEvent } from "../../../../../../common/dom/fire_event";
import "../../../../../../components/ha-area-picker";
import "../../../../../../components/input/ha-input";
import "../../../../../../components/ha-select";
import "../../../../../../components/ha-dropdown-item";
import type { HaSelectSelectEvent } from "../../../../../../components/ha-select";
import type { ExtEntityRegistryEntry } from "../../../../../../data/entity/entity_registry";
import type { DeviceRegistryEntry } from "../../../../../../data/device/device_registry";
import type { HomeAssistant } from "../../../../../../types";
import { brandsUrl } from "../../../../../../util/brands-url";
import { sharedStyles } from "./matter-add-device-shared-styles";
import { OVERRIDE_DEVICE_CLASSES } from "../../../../entities/entity-registry-settings-editor";
declare global {
interface HASSDomEvents {
"device-added-changed": {
name: string;
area: string | undefined;
deviceClass: string | undefined;
hasPendingUpdates: boolean;
};
}
}
@customElement("matter-add-device-device-added")
class MatterAddDeviceDeviceAdded extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public device!: DeviceRegistryEntry;
@property({ attribute: false }) public mainEntity?: ExtEntityRegistryEntry;
@state() private _deviceName = "";
@state() private _area: string | undefined;
@state() private _deviceClass: string | undefined;
private _initialized = false;
private _deviceClassInitialized = false;
protected willUpdate(changedProps: PropertyValues) {
if (!this._initialized && this.device) {
this._initialized = true;
this._deviceName = computeDeviceName(this.device) ?? "";
this._area = this.device.area_id ?? undefined;
}
if (
!this._deviceClassInitialized &&
(changedProps.has("mainEntity") || this._initialized) &&
this.mainEntity
) {
this._deviceClassInitialized = true;
this._deviceClass =
this.mainEntity.device_class ??
this.mainEntity.original_device_class ??
undefined;
}
}
private get _deviceClassOptions(): string[][] | undefined {
if (!this.mainEntity) return undefined;
const domain = computeDomain(this.mainEntity.entity_id);
const deviceClasses = OVERRIDE_DEVICE_CLASSES[domain];
if (!deviceClasses) return undefined;
const options: string[][] = [[], []];
for (const deviceClass of deviceClasses) {
if (
this.mainEntity.original_device_class &&
deviceClass.includes(this.mainEntity.original_device_class)
) {
options[0] = deviceClass;
} else {
options[1].push(...deviceClass);
}
}
return options;
}
private get _hasPendingUpdates(): boolean {
const origName = computeDeviceName(this.device) ?? "";
const origArea = this.device.area_id ?? undefined;
const origDeviceClass =
this.mainEntity?.device_class ??
this.mainEntity?.original_device_class ??
undefined;
return (
this._deviceName !== origName ||
this._area !== origArea ||
(this.mainEntity !== undefined && this._deviceClass !== origDeviceClass)
);
}
protected updated(changedProps: Map<string, unknown>) {
if (
changedProps.has("_deviceName") ||
changedProps.has("_area") ||
changedProps.has("_deviceClass")
) {
fireEvent(this, "device-added-changed", {
name: this._deviceName,
area: this._area,
deviceClass: this._deviceClass,
hasPendingUpdates: this._hasPendingUpdates,
});
}
}
private _deviceClassesSorted = memoizeOne(
(domain: string, deviceClasses: string[]) =>
deviceClasses
.map((deviceClass) => ({
deviceClass,
label: this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${deviceClass}`
),
}))
.sort((a, b) =>
caseInsensitiveStringCompare(
a.label,
b.label,
this.hass.locale.language
)
)
);
protected render() {
if (!this.device) return nothing;
const domain = this.mainEntity
? computeDomain(this.mainEntity.entity_id)
: undefined;
const deviceClassOptions = this._deviceClassOptions;
return html`
<div class="content">
<div class="device">
<div class="device-info">
<img
alt="Matter"
src=${brandsUrl(
{
domain: "matter",
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
},
this.hass.auth.data.hassUrl
)}
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
<div class="device-name">
<span>${computeDeviceName(this.device)}</span>
<span class="secondary">Matter</span>
</div>
</div>
<ha-input
.label=${this.hass.localize(
"ui.panel.config.integrations.config_flow.device_name"
)}
.value=${this._deviceName}
@change=${this._deviceNameChanged}
></ha-input>
<ha-area-picker
.hass=${this.hass}
.value=${this._area}
@value-changed=${this._areaPicked}
></ha-area-picker>
${deviceClassOptions && domain
? html`
<ha-select
.label=${this.hass.localize(
"ui.dialogs.entity_registry.editor.device_class"
)}
.value=${this._deviceClass
? this.hass.localize(
`ui.dialogs.entity_registry.editor.device_classes.${domain}.${this._deviceClass}`
)
: undefined}
clearable
@selected=${this._deviceClassChanged}
>
${this._deviceClassesSorted(
domain,
deviceClassOptions[0]
).map(
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
.selected=${entry.deviceClass === this._deviceClass}
>
${entry.label}
</ha-dropdown-item>
`
)}
${deviceClassOptions[0].length && deviceClassOptions[1].length
? html`<wa-divider></wa-divider>`
: nothing}
${this._deviceClassesSorted(
domain,
deviceClassOptions[1]
).map(
(entry) => html`
<ha-dropdown-item
.value=${entry.deviceClass}
.selected=${entry.deviceClass === this._deviceClass}
>
${entry.label}
</ha-dropdown-item>
`
)}
</ha-select>
`
: nothing}
</div>
</div>
`;
}
private _deviceNameChanged(ev: InputEvent) {
this._deviceName = (ev.currentTarget as HTMLInputElement).value;
}
private _areaPicked(ev: CustomEvent<{ value: string }>) {
this._area = ev.detail.value || undefined;
}
private _deviceClassChanged(ev: HaSelectSelectEvent<string, true>) {
this._deviceClass = ev.detail.value;
}
static styles = [
sharedStyles,
css`
.device {
border: 1px solid var(--divider-color);
padding: var(--ha-space-2);
border-radius: var(--ha-border-radius-sm);
}
.device-info {
display: flex;
align-items: center;
gap: var(--ha-space-2);
margin-bottom: var(--ha-space-1);
}
.device-info img {
width: 40px;
height: 40px;
}
.device-name {
display: flex;
flex-direction: column;
justify-content: center;
}
.secondary {
color: var(--secondary-text-color);
font-size: var(--ha-font-size-s);
}
ha-input {
margin: var(--ha-space-2) 0;
}
ha-area-picker,
ha-select {
display: block;
margin-top: var(--ha-space-2);
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"matter-add-device-device-added": MatterAddDeviceDeviceAdded;
}
}
@@ -85,6 +85,12 @@ class MatterAddDeviceNew extends LitElement {
static styles = [
sharedStyles,
css`
.content {
display: flex;
align-items: center;
flex-direction: column;
text-align: center;
}
.app-qr {
margin: 24px auto 0 auto;
display: flex;
@@ -506,7 +506,6 @@ export class ThreadConfigPanel extends SubscribeMixin(LitElement) {
this._refresh();
},
startFlowHandler: "otbr",
showAdvanced: this.hass.userData?.showAdvanced,
});
}
@@ -27,8 +27,6 @@ class HaConfigScript 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 scripts: ScriptEntity[] = [];
@@ -78,7 +76,6 @@ class HaConfigScript 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) {
+18 -18
View File
@@ -71,7 +71,7 @@ export default class HaScriptFieldRow extends LitElement {
const hasSelector =
this.field.selector && typeof this.field.selector === "object";
const commentTooltipText = truncateWithEllipsis(
const noteTooltipText = truncateWithEllipsis(
this.field.description?.trim() || "",
250
);
@@ -101,13 +101,13 @@ export default class HaScriptFieldRow extends LitElement {
.path=${mdiDotsVertical}
></ha-icon-button>
<ha-dropdown-item value="edit_comment">
<ha-dropdown-item value="edit_note">
<ha-svg-icon
slot="icon"
.path=${mdiCommentEditOutline}
></ha-svg-icon>
${this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.field.description ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.field.description ? "edit" : "add"}`
)}
</ha-dropdown-item>
<ha-dropdown-item value="toggle_yaml_mode">
@@ -157,15 +157,15 @@ export default class HaScriptFieldRow extends LitElement {
${this.field.description?.trim()
? html`
<ha-svg-icon
id="comment-icon"
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}
@@ -361,25 +361,25 @@ export default class HaScriptFieldRow extends LitElement {
});
}
private _editComment = async (): Promise<void> => {
const comment = await showPromptDialog(this, {
private _editNote = async (): Promise<void> => {
const note = await showPromptDialog(this, {
title: this.hass.localize(
`ui.panel.config.automation.editor.comment.${this.field.description ? "edit" : "add"}`
`ui.panel.config.automation.editor.note.${this.field.description ? "edit" : "add"}`
),
inputLabel: this.hass.localize(
"ui.panel.config.automation.editor.comment.label"
"ui.panel.config.automation.editor.note.label"
),
inputType: "string",
defaultValue: this.field.description,
confirmText: this.hass.localize("ui.common.submit"),
multiline: true,
});
if (comment !== null) {
if (note !== null) {
const value = { ...this.field };
if (comment === "") {
if (note === "") {
delete value.description;
} else {
value.description = comment;
value.description = note;
}
fireEvent(this, "value-changed", {
value,
@@ -431,7 +431,7 @@ export default class HaScriptFieldRow extends LitElement {
excludeKeys: this.excludeKeys,
},
yamlMode: this._yamlMode,
editComment: this._editComment,
editNote: this._editNote,
} satisfies ScriptFieldSidebarConfig);
if (this.narrow) {
@@ -492,8 +492,8 @@ export default class HaScriptFieldRow extends LitElement {
case "delete":
this._onDelete();
break;
case "edit_comment":
this._editComment();
case "edit_note":
this._editNote();
break;
}
}
+2 -2
View File
@@ -4,7 +4,7 @@ import { ifDefined } from "lit/directives/if-defined";
import "../../layouts/hass-error-screen";
import "../../layouts/hass-subpage";
import type { HomeAssistant, PanelInfo } from "../../types";
import { IFRAME_SANDBOX_SAME_ORIGIN } from "../../util/iframe";
import { IFRAME_SANDBOX } from "../../util/iframe";
@customElement("ha-panel-iframe")
class HaPanelIframe extends LitElement {
@@ -41,7 +41,7 @@ class HaPanelIframe extends LitElement {
this.panel.title === null ? undefined : this.panel.title
)}
src=${this.panel.config.url}
.sandbox=${IFRAME_SANDBOX_SAME_ORIGIN}
.sandbox=${IFRAME_SANDBOX}
allow="fullscreen"
></iframe>
</hass-subpage>
+30 -7
View File
@@ -1,3 +1,5 @@
import { ensureArray } from "../../../common/array/ensure-array";
import { customCards } from "../../../data/lovelace_custom_cards";
import type { HomeAssistant } from "../../../types";
import { CARD_SUGGESTION_PROVIDERS } from "./registry";
import type { CardSuggestion } from "./types";
@@ -5,18 +7,39 @@ import type { CardSuggestion } from "./types";
export type { CardSuggestion, CardSuggestionProvider } from "./types";
export { CARD_SUGGESTION_PROVIDERS } from "./registry";
export interface CardSuggestions {
core: CardSuggestion[];
custom: CardSuggestion[];
}
export const generateCardSuggestions = (
hass: HomeAssistant,
entityId: string | undefined
): CardSuggestion[] => {
if (!entityId || hass.states[entityId] === undefined) return [];
return Object.values(CARD_SUGGESTION_PROVIDERS).flatMap((provider) => {
): CardSuggestions => {
if (!entityId || hass.states[entityId] === undefined) {
return { core: [], custom: [] };
}
const core = Object.values(CARD_SUGGESTION_PROVIDERS).flatMap((provider) => {
try {
const result = provider.getEntitySuggestion(hass, entityId);
if (!result) return [];
return Array.isArray(result) ? result : [result];
} catch {
return ensureArray(provider.getEntitySuggestion(hass, entityId)) ?? [];
} catch (err) {
// eslint-disable-next-line no-console
console.error("Card suggestion provider threw:", err);
return [];
}
});
const custom = customCards.flatMap((card) => {
if (!card.getEntitySuggestion) return [];
try {
return ensureArray(card.getEntitySuggestion(hass, entityId)) ?? [];
} catch (err) {
// eslint-disable-next-line no-console
console.error(
`Custom card "${card.type}" getEntitySuggestion threw:`,
err
);
return [];
}
});
return { core, custom };
};
@@ -112,12 +112,28 @@ export function getCommonOptions(
yAxisFractionDigits = 1
): ECOption {
const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData);
const suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
let suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
const compare = compareStart !== undefined && compareEnd !== undefined;
const showCompareYear =
compare && start.getFullYear() !== compareStart.getFullYear();
// Extend suggestedMax so compare bars that land past the main end
// (e.g. Feb compared to Jan) stay visible instead of being clipped.
if (compare) {
const transformedCompareEnd = getCompareTransform(
start,
compareStart
)(compareEnd);
if (transformedCompareEnd.getTime() > suggestedMax.getTime()) {
suggestedMax = getSuggestedMax(
suggestedPeriod,
transformedCompareEnd,
detailedDailyData
);
}
}
const monthTimeAxis: ECOption = {
xAxis: {
type: "time",
@@ -351,21 +367,35 @@ export function getCompareTransform(start: Date, compareStart?: Date) {
if (!compareStart) {
return (ts: Date) => ts;
}
const compareDayDiff = differenceInDays(start, compareStart);
const compareYearDiff = differenceInYears(start, compareStart);
if (
compareYearDiff !== 0 &&
start.getTime() === startOfYear(start).getTime()
) {
return (ts: Date) => addYears(ts, compareYearDiff);
// addYears clamps Feb 29 -> Feb 28 across leap-year boundaries; fall back
// to a day-shift so each compare day keeps a unique x position.
return (ts: Date) => {
const shifted = addYears(ts, compareYearDiff);
return shifted.getDate() === ts.getDate()
? shifted
: addDays(ts, compareDayDiff);
};
}
const compareMonthDiff = differenceInMonths(start, compareStart);
if (
compareMonthDiff !== 0 &&
start.getTime() === startOfMonth(start).getTime()
) {
return (ts: Date) => addMonths(ts, compareMonthDiff);
// addMonths clamps Jan 31 -> Feb 28 when shifting between unequal-length
// months; fall back to a day-shift so each compare day keeps a unique x.
return (ts: Date) => {
const shifted = addMonths(ts, compareMonthDiff);
return shifted.getDate() === ts.getDate()
? shifted
: addDays(ts, compareDayDiff);
};
}
const compareDayDiff = differenceInDays(start, compareStart);
if (compareDayDiff !== 0 && start.getTime() === startOfDay(start).getTime()) {
return (ts: Date) => addDays(ts, compareDayDiff);
}
@@ -143,11 +143,7 @@ export class HuiEnergyCompareCard
);
return html`
<ha-alert
dismissable
.localize=${this.hass.localize}
@alert-dismissed-clicked=${this._stopCompare}
>
<ha-alert dismissable @alert-dismissed-clicked=${this._stopCompare}>
${this.hass.localize(
"ui.panel.lovelace.cards.energy.energy_compare.info",
{
+2 -2
View File
@@ -13,7 +13,7 @@ import type {
LovelaceGridOptions,
} from "../types";
import type { IframeCardConfig } from "./types";
import { IFRAME_SANDBOX_SAME_ORIGIN } from "../../../util/iframe";
import { IFRAME_SANDBOX } from "../../../util/iframe";
@customElement("hui-iframe-card")
export class HuiIframeCard extends LitElement implements LovelaceCard {
@@ -95,7 +95,7 @@ export class HuiIframeCard extends LitElement implements LovelaceCard {
}
const sandbox_params = this._config.disable_sandbox
? undefined
: `${sandbox_user_params} ${IFRAME_SANDBOX_SAME_ORIGIN}`;
: `${sandbox_user_params} ${IFRAME_SANDBOX}`;
return html`
<ha-card
@@ -3,6 +3,11 @@ import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-ripple";
import {
getCustomCardEntry,
isCustomType,
stripCustomPrefix,
} from "../../../../data/lovelace_custom_cards";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../../types";
import "../../cards/hui-card";
@@ -52,9 +57,16 @@ export class HuiSuggestionCard extends LitElement {
protected render(): TemplateResult {
const { suggestion } = this;
const hiddenCount = this._preview?.hiddenCount ?? 0;
const cardName = this.hass.localize(
`ui.panel.lovelace.editor.card.${suggestion.config.type}.name` as any
);
const type = suggestion.config.type;
let cardName: string;
if (isCustomType(type)) {
const customType = stripCustomPrefix(type);
cardName = getCustomCardEntry(customType)?.name ?? customType;
} else {
cardName = this.hass.localize(
`ui.panel.lovelace.editor.card.${type}.name` as any
);
}
const label = suggestion.label
? `${cardName} - ${suggestion.label}`
: cardName;
@@ -17,7 +17,10 @@ import "../../../../components/ha-svg-icon";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import { haStyleScrollbar } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { generateCardSuggestions } from "../../card-suggestions";
import {
generateCardSuggestions,
type CardSuggestions,
} from "../../card-suggestions";
import type { CardSuggestion } from "../../card-suggestions/types";
import "./hui-suggestion-card";
import "./hui-suggestion-entity-tree";
@@ -58,18 +61,21 @@ export class HuiSuggestionPicker extends LitElement {
(
entityId: string | undefined,
priorityTypesKey: string
): CardSuggestion[] => {
const suggestions = generateCardSuggestions(this.hass, entityId);
): CardSuggestions => {
const { core, custom } = generateCardSuggestions(this.hass, entityId);
const priorityTypes = priorityTypesKey
? priorityTypesKey.split("|")
: undefined;
if (!priorityTypes?.length) return suggestions;
if (!priorityTypes?.length) return { core, custom };
const isPrioritized = (s: CardSuggestion) =>
priorityTypes.includes(s.config.type);
return [
...suggestions.filter(isPrioritized),
...suggestions.filter((s) => !isPrioritized(s)),
];
return {
core: [
...core.filter(isPrioritized),
...core.filter((s) => !isPrioritized(s)),
],
custom,
};
}
);
@@ -101,18 +107,43 @@ export class HuiSuggestionPicker extends LitElement {
hasEntity: boolean
): TemplateResult | typeof nothing {
if (!hasEntity) return this._renderEmptyState();
if (this._narrow) {
return html`
${this._renderSelectedEntity()}
<ha-section-title>
const { core, custom } = this._suggestions();
return html`
${this._narrow ? this._renderSelectedEntity() : nothing}
<ha-section-title>
${this.hass.localize(
"ui.panel.lovelace.editor.cardpicker.suggestions_title"
)}
</ha-section-title>
${this._renderSuggestionsGrid(core)}
${custom.length
? html`
<ha-section-title>
${this.hass.localize(
"ui.panel.lovelace.editor.cardpicker.community_title"
)}
</ha-section-title>
${this._renderSuggestionsGrid(custom)}
`
: nothing}
${this._renderBrowseCard()}
`;
}
private _renderBrowseCard(): TemplateResult {
return html`
<div class="browse-card">
<p>
${this.hass.localize("ui.panel.lovelace.editor.cardpicker.not_found")}
</p>
<ha-button appearance="plain" @click=${this._browseCards}>
<ha-svg-icon slot="start" .path=${mdiViewGridPlus}></ha-svg-icon>
${this.hass.localize(
"ui.panel.lovelace.editor.cardpicker.suggestions_title"
"ui.panel.lovelace.editor.cardpicker.browse_cards"
)}
</ha-section-title>
${this._renderSuggestionsGrid(this._suggestions())}
`;
}
return this._renderSuggestionsGrid(this._suggestions());
</ha-button>
</div>
`;
}
private _renderSelectedEntity(): TemplateResult {
@@ -171,6 +202,17 @@ export class HuiSuggestionPicker extends LitElement {
`;
}
private _suggestionKeys = new WeakMap<CardSuggestion, string>();
private _suggestionKey = (s: CardSuggestion): string => {
let key = this._suggestionKeys.get(s);
if (key === undefined) {
key = JSON.stringify(s.config);
this._suggestionKeys.set(s, key);
}
return key;
};
private _renderSuggestionsGrid(
suggestions: CardSuggestion[]
): TemplateResult {
@@ -178,7 +220,7 @@ export class HuiSuggestionPicker extends LitElement {
<div class="suggestions" @pick-suggestion=${this._pickSuggestion}>
${repeat(
suggestions,
(s: CardSuggestion) => JSON.stringify(s.config),
this._suggestionKey,
(s: CardSuggestion) => html`
<hui-suggestion-card
.hass=${this.hass}
@@ -186,34 +228,11 @@ export class HuiSuggestionPicker extends LitElement {
></hui-suggestion-card>
`
)}
<div
class="browse-card"
tabindex="0"
role="button"
aria-label=${this.hass.localize(
"ui.panel.lovelace.editor.cardpicker.browse_cards"
)}
@click=${this._browseCards}
@keydown=${this._browseCardsKeydown}
>
<ha-svg-icon .path=${mdiViewGridPlus}></ha-svg-icon>
<span class="browse-card-title">
${this.hass.localize(
"ui.panel.lovelace.editor.cardpicker.browse_cards"
)}
</span>
<p>
${this.hass.localize(
"ui.panel.lovelace.editor.cardpicker.not_found"
)}
</p>
<ha-ripple></ha-ripple>
</div>
</div>
`;
}
private _suggestions(): CardSuggestion[] {
private _suggestions(): CardSuggestions {
return this._computeSuggestions(
this._entityId,
(this.prioritizedCardTypes ?? []).join("|")
@@ -224,13 +243,6 @@ export class HuiSuggestionPicker extends LitElement {
fireEvent(this, "browse-cards", undefined);
}
private _browseCardsKeydown(ev: KeyboardEvent): void {
if (ev.key === "Enter" || ev.key === " ") {
ev.preventDefault();
this._browseCards();
}
}
private _handleEntityPicked(ev: CustomEvent<{ entityId: string }>): void {
this._entityId = ev.detail.entityId;
}
@@ -313,37 +325,11 @@ export class HuiSuggestionPicker extends LitElement {
line-height: var(--ha-line-height-expanded);
}
.browse-card {
position: relative;
box-sizing: border-box;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--ha-space-2);
padding: var(--ha-space-6);
text-align: center;
cursor: pointer;
overflow: hidden;
border-radius: var(
--ha-card-border-radius,
var(--ha-border-radius-lg)
);
border: var(--ha-card-border-width, 1px) dashed
var(--ha-card-border-color, var(--divider-color));
background: var(--primary-background-color, #fafafa);
color: var(--primary-text-color);
}
.browse-card:focus-visible {
outline: 2px solid var(--primary-color);
outline-offset: 2px;
}
.browse-card ha-svg-icon {
color: var(--ha-color-text-secondary);
}
.browse-card-title {
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-medium);
padding: var(--ha-space-6) var(--ha-space-4);
}
.browse-card p {
margin: 0;
@@ -106,7 +106,10 @@ export class HaCardConditionEditor extends LitElement {
@state() private _testingResult?: boolean;
@state() private _liveTestResult: LiveTestState = "unknown";
@state() private _liveTestResult: {
state: LiveTestState;
message?: string;
} = { state: "unknown" };
private _listeners = new ConditionListenersController(this);
@@ -175,7 +178,7 @@ export class HaCardConditionEditor extends LitElement {
private _evaluateLiveTest() {
if (!this.condition || !this._condition) {
this._liveTestResult = "unknown";
this._liveTestResult = { state: "unknown" };
return;
}
@@ -183,12 +186,22 @@ export class HaCardConditionEditor extends LitElement {
isNoEntityCondition(this._condition.condition, this._noEntity) ||
containsNoEntityCondition(this._condition, this._noEntity)
) {
this._liveTestResult = "unknown";
this._liveTestResult = {
state: "unknown",
message: this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.live_test_state.unknown"
),
};
return;
}
if (!validateConditionalConfig([this.condition])) {
this._liveTestResult = "invalid";
this._liveTestResult = {
state: "invalid",
message: this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.live_test_state.invalid"
),
};
return;
}
@@ -197,7 +210,12 @@ export class HaCardConditionEditor extends LitElement {
? { entity_id: this._entityContext.entityId }
: {};
const pass = checkConditionsMet([this.condition], this.hass, testContext);
this._liveTestResult = pass ? "pass" : "fail";
this._liveTestResult = {
state: pass ? "pass" : "fail",
message: this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${pass ? "pass" : "fail"}`
),
};
}
protected render() {
@@ -242,10 +260,11 @@ export class HaCardConditionEditor extends LitElement {
: html`
<ha-automation-row-live-test
slot="icons"
.state=${this._liveTestResult}
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult}`
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
)}
.message=${this._liveTestResult.message}
></ha-automation-row-live-test>
`}
<ha-dropdown
@@ -1,78 +0,0 @@
import type { TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import "../../components/ha-alert";
import "../../components/ha-card";
import "../../components/ha-switch";
import "../../components/item/ha-row-item";
import type { CoreFrontendUserData } from "../../data/frontend";
import { saveFrontendUserData } from "../../data/frontend";
import type { HomeAssistant } from "../../types";
import { documentationUrl } from "../../util/documentation-url";
@customElement("ha-advanced-mode-row")
class AdvancedModeRow extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public coreUserData?: CoreFrontendUserData;
@state() private _error?: string;
protected render(): TemplateResult {
return html`
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: nothing}
<ha-row-item>
<span slot="headline"
>${this.hass.localize("ui.panel.profile.advanced_mode.title")}</span
>
<span slot="supporting-text"
>${this.hass.localize("ui.panel.profile.advanced_mode.description")}
<a
href=${documentationUrl(
this.hass,
"/blog/2019/07/17/release-96/#advanced-mode"
)}
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.panel.profile.advanced_mode.link_promo")}
</a></span
>
<ha-switch
slot="end"
.checked=${!!this.coreUserData && !!this.coreUserData.showAdvanced}
.disabled=${this.coreUserData === undefined}
@change=${this._advancedToggled}
></ha-switch>
</ha-row-item>
`;
}
private async _advancedToggled(ev) {
try {
saveFrontendUserData(this.hass.connection, "core", {
...this.coreUserData,
showAdvanced: ev.currentTarget.checked,
});
} catch (err: any) {
this._error = err.message || err;
}
}
static styles = css`
a {
color: var(--primary-color);
}
ha-alert {
margin: 0 16px;
display: block;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-advanced-mode-row": AdvancedModeRow;
}
}
@@ -17,7 +17,6 @@ import "../../layouts/hass-tabs-subpage";
import { haStyle } from "../../resources/styles";
import type { HomeAssistant, Route } from "../../types";
import { isMobileClient } from "../../util/is_mobile";
import "./ha-advanced-mode-row";
import "./ha-enable-shortcuts-row";
import "./ha-entity-id-picker-row";
import "./ha-force-narrow-row";
@@ -166,14 +165,6 @@ class HaProfileSectionGeneral extends LitElement {
)}
</ha-button>
</ha-row-item>
${this.hass.user!.is_admin
? html`
<ha-advanced-mode-row
.hass=${this.hass}
.coreUserData=${this._coreUserData}
></ha-advanced-mode-row>
`
: nothing}
${this.hass.user!.is_admin
? html`
<ha-entity-id-picker-row
@@ -289,11 +280,6 @@ class HaProfileSectionGeneral extends LitElement {
display: block;
margin: 24px 0;
}
.promo-advanced {
text-align: center;
color: var(--secondary-text-color);
}
`,
];
}
-1
View File
@@ -285,7 +285,6 @@ class PanelTodo extends LitElement {
private async _addList(): Promise<void> {
showConfigFlowDialog(this, {
startFlowHandler: "local_todo",
showAdvanced: this.hass.userData?.showAdvanced,
manifest: await fetchIntegrationManifest(this.hass, "local_todo"),
});
}

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