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