Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de0a322c52 | |||
| f37cf1e848 | |||
| a188ef1b7a | |||
| 087ef159df | |||
| e39e1b3f5b | |||
| ff583d2274 | |||
| d4de29e073 | |||
| 97dfed0cc4 | |||
| 8b3df752da | |||
| 8c0d547962 | |||
| 5e3d84f0ad | |||
| b4e30bdf63 | |||
| 4fcae4231c | |||
| 2aecf33955 | |||
| 5f26a2b3da | |||
| b08f5bcb34 | |||
| c329e5b827 | |||
| 97f591337d | |||
| e6e6e75f73 | |||
| ff334de0ca | |||
| 8dbe97b480 | |||
| 7bea54851d | |||
| 9298e00f20 | |||
| 70085d4bad | |||
| d83a553b62 | |||
| cab5c6af30 | |||
| d44d8a6dbd | |||
| 3cf1d94b92 | |||
| 9f5f849e32 | |||
| 27e9926363 | |||
| efe734892a | |||
| b3d79e312d | |||
| ecfef9e112 | |||
| ca960446f0 | |||
| a6eb722025 | |||
| f3ff01ace4 | |||
| d5e1a373ec | |||
| e1b9a1a185 | |||
| efe8eaa941 | |||
| 5856196ef3 | |||
| 2671a8c64b | |||
| 8620653a54 | |||
| c4f4cbd323 | |||
| 2e0df00f0f | |||
| ce02f8072d | |||
| c973aa7516 | |||
| 1e2328707c | |||
| 56368b88cd | |||
| fcd4f177c1 | |||
| 7423ae7316 | |||
| 4427c581f1 | |||
| cf86bb9821 | |||
| 897802dc16 | |||
| 95edd6c2c2 | |||
| dd65173c5a | |||
| cf26753f7d | |||
| d6ab8ffb16 | |||
| 2dc4b16eac | |||
| 1eba765bc2 | |||
| 398479ddd7 | |||
| c4fd7bb3e1 | |||
| 4cfc67a95e | |||
| e38d1964ca | |||
| ec8b5c77bd | |||
| 425f2775e2 | |||
| 3a3d8191a3 | |||
| 04fca68549 | |||
| 3046f3e47d | |||
| 35601a0900 | |||
| e7016c15af | |||
| 624521e30b | |||
| 4876bfa639 | |||
| 5dea0764b2 | |||
| 121ed7ac1f |
|
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 |
@@ -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}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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}
|
||||
|
||||
@@ -20,6 +20,7 @@ export class HaCheckListItem extends CheckListItemBase {
|
||||
separateCheckboxClick = false;
|
||||
|
||||
async onChange(event) {
|
||||
event.stopPropagation();
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"] {
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -40,7 +40,6 @@ export const createConfigFlow = (
|
||||
"config/config_entries/flow",
|
||||
{
|
||||
handler,
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
entry_id,
|
||||
},
|
||||
HEADERS
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
|
||||
export interface DeviceAutomation {
|
||||
alias?: string;
|
||||
comment?: string;
|
||||
note?: string;
|
||||
device_id: string;
|
||||
domain: string;
|
||||
entity_id?: string;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ export const createOptionsFlow = (hass: HomeAssistant, handler: string) =>
|
||||
"config/config_entries/options/flow",
|
||||
{
|
||||
handler,
|
||||
show_advanced_options: Boolean(hass.userData?.showAdvanced),
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
}
|
||||
|
||||