Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| de487d788a |
@@ -27,7 +27,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@babel/runtime": "7.29.2",
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.2",
|
||||
"@codemirror/commands": "6.10.3",
|
||||
@@ -126,10 +126,10 @@
|
||||
"xss": "1.0.15"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.29.7",
|
||||
"@babel/core": "7.29.0",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.8",
|
||||
"@babel/plugin-transform-runtime": "7.29.7",
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.5",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.22.1",
|
||||
"@eslint/js": "10.0.1",
|
||||
"@html-eslint/eslint-plugin": "0.61.0",
|
||||
|
||||
|
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 |
|
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 |
@@ -14,7 +14,6 @@ import type {
|
||||
ECElementEvent,
|
||||
LegendComponentOption,
|
||||
LineSeriesOption,
|
||||
TooltipOption,
|
||||
XAXisOption,
|
||||
YAXisOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
@@ -30,59 +29,22 @@ 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,
|
||||
HaECOption,
|
||||
HaECSeries,
|
||||
HaECSeriesItem,
|
||||
HaTooltipOption,
|
||||
} from "../../resources/echarts/echarts";
|
||||
import type { ECOption } 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?: {
|
||||
@@ -104,9 +66,9 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data: HaECSeries = [];
|
||||
@property({ attribute: false }) public data: ECOption["series"] = [];
|
||||
|
||||
@property({ attribute: false }) public options?: HaECOption;
|
||||
@property({ attribute: false }) public options?: ECOption;
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@@ -652,7 +614,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
// Return an array of all IDs associated with the legend item of the primaryId
|
||||
private _getAllIdsFromLegend(
|
||||
options: HaECOption | undefined,
|
||||
options: ECOption | undefined,
|
||||
primaryId: string
|
||||
): string[] {
|
||||
if (!options) return [primaryId];
|
||||
@@ -672,7 +634,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: HaECOption | undefined) {
|
||||
private _updateHiddenStatsFromOptions(options: ECOption | undefined) {
|
||||
if (!options) return;
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
@@ -795,34 +757,22 @@ export class HaChartBase extends LitElement {
|
||||
xAxis,
|
||||
};
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
return options as ECOption;
|
||||
return options;
|
||||
}
|
||||
|
||||
private _createTheme(style: CSSStyleDeclaration) {
|
||||
@@ -1010,12 +960,8 @@ export class HaChartBase extends LitElement {
|
||||
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 ((s as LineSeriesOption).sampling === "minmax") {
|
||||
if (s.sampling === "minmax") {
|
||||
const minX = xAxis?.min
|
||||
? xAxis.min instanceof Date
|
||||
? xAxis.min.getTime()
|
||||
@@ -1030,8 +976,8 @@ export class HaChartBase extends LitElement {
|
||||
? xAxis.max
|
||||
: undefined
|
||||
: undefined;
|
||||
result = {
|
||||
...result,
|
||||
return {
|
||||
...s,
|
||||
sampling: undefined,
|
||||
data: downSampleLineData(
|
||||
data as LineSeriesOption["data"],
|
||||
@@ -1039,10 +985,11 @@ export class HaChartBase extends LitElement {
|
||||
minX,
|
||||
maxX
|
||||
),
|
||||
} as HaECSeriesItem;
|
||||
};
|
||||
}
|
||||
}
|
||||
return processSeriesTooltipFormatter(result);
|
||||
const name = filterXSS(String(s.name ?? s.id ?? ""));
|
||||
return { ...s, name, data };
|
||||
});
|
||||
return series as ECOption["series"];
|
||||
}
|
||||
@@ -1379,8 +1326,8 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _compareCustomLegendOptions(
|
||||
oldOptions: HaECOption | undefined,
|
||||
newOptions: HaECOption | undefined
|
||||
oldOptions: ECOption | undefined,
|
||||
newOptions: ECOption | undefined
|
||||
): boolean {
|
||||
const oldLegends = ensureArray(
|
||||
oldOptions?.legend || []
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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, TemplateResult } from "lit";
|
||||
import type { PropertyValues } 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 { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } 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
|
||||
) => TemplateResult | typeof nothing | null;
|
||||
) => string;
|
||||
|
||||
/**
|
||||
* 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"]): HaECOption => ({
|
||||
(categories?: NetworkData["categories"]): ECOption => ({
|
||||
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 { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } 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: HaECOption = {
|
||||
const options = {
|
||||
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}
|
||||
@@ -103,16 +103,12 @@ export class HaSankeyChart extends LitElement {
|
||||
: data.value;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${node?.label ?? data.id}<br />${value}`;
|
||||
return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`;
|
||||
}
|
||||
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 html`${source?.label ?? data.source} →
|
||||
${target?.label ?? data.target}<br />${value}`;
|
||||
return `${filterXSS(source?.label ?? data.source)} → ${filterXSS(target?.label ?? data.target)}<br>${value}`;
|
||||
}
|
||||
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 type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import type { ECOption } 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: HaECOption = {
|
||||
const options = {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
};
|
||||
} as ECOption;
|
||||
|
||||
return html`<ha-chart-base
|
||||
.data=${this._createData(this.data)}
|
||||
@@ -71,10 +71,7 @@ export class HaSunburstChart extends LitElement {
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${data.name}<br />${value}`;
|
||||
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
|
||||
};
|
||||
|
||||
private _createData = memoizeOne(
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
@@ -12,9 +12,8 @@ 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 { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
getNumberFormatOptions,
|
||||
@@ -25,6 +24,7 @@ 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?: HaECOption;
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
private _hiddenStats = new Set<string>();
|
||||
|
||||
@@ -141,11 +141,12 @@ 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
|
||||
);
|
||||
const title =
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>";
|
||||
const datapoints: Record<string, any>[] = [];
|
||||
this._chartData.forEach((dataset, index) => {
|
||||
if (
|
||||
@@ -176,44 +177,52 @@ export class StateHistoryChartLine extends LitElement {
|
||||
seriesName: dataset.name,
|
||||
seriesIndex: index,
|
||||
value: lastData,
|
||||
color: dataset.color,
|
||||
// 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>`,
|
||||
});
|
||||
});
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
|
||||
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}`;
|
||||
})}`;
|
||||
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>")
|
||||
);
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { css, html, LitElement } 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";
|
||||
@@ -14,9 +15,8 @@ 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 { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } 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?: HaECOption;
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
@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 HaECSeries}
|
||||
.data=${this._chartData as ECOption["series"]}
|
||||
small-controls
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
@@ -132,35 +132,42 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
return rect;
|
||||
};
|
||||
|
||||
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)}`;
|
||||
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)}`;
|
||||
|
||||
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}`;
|
||||
};
|
||||
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("");
|
||||
};
|
||||
|
||||
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, nothing } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -34,13 +34,12 @@ import {
|
||||
isExternalStatistic,
|
||||
statisticsHaveType,
|
||||
} from "../../data/recorder";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { ECOption } 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";
|
||||
|
||||
@@ -127,7 +126,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _statisticIds: string[] = [];
|
||||
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
@state() private _chartOptions?: ECOption;
|
||||
|
||||
@state() private _hiddenStats = new Set<string>();
|
||||
|
||||
@@ -252,101 +251,91 @@ export class StatisticsChart extends LitElement {
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
const rows: {
|
||||
time?: string;
|
||||
color: string;
|
||||
seriesName?: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
for (const param of params) {
|
||||
if (rendered[param.seriesIndex]) continue;
|
||||
rendered[param.seriesIndex] = true;
|
||||
return params
|
||||
.map((param, index: number) => {
|
||||
if (rendered[param.seriesIndex]) return "";
|
||||
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]);
|
||||
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>";
|
||||
}
|
||||
} else {
|
||||
startTime = new Date(param.value[0]);
|
||||
// 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>`;
|
||||
}
|
||||
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]),
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(
|
||||
rawValue,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
}
|
||||
options
|
||||
)}${unit}`;
|
||||
|
||||
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}`
|
||||
)}`;
|
||||
const time = index === 0 ? rawTime : "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("<br>");
|
||||
};
|
||||
|
||||
private _createOptions() {
|
||||
|
||||
@@ -20,7 +20,6 @@ export class HaCheckListItem extends CheckListItemBase {
|
||||
separateCheckboxClick = false;
|
||||
|
||||
async onChange(event) {
|
||||
event.stopPropagation();
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
@@ -1,24 +1,31 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { until } from "lit/directives/until";
|
||||
import { computeDomain } from "../common/entity/compute_domain";
|
||||
import { configContext, connectionContext } from "../data/context";
|
||||
import {
|
||||
DEFAULT_SERVICE_ICON,
|
||||
FALLBACK_DOMAIN_ICONS,
|
||||
serviceIcon,
|
||||
} from "../data/icons";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-icon";
|
||||
import "./ha-svg-icon";
|
||||
|
||||
@customElement("ha-service-icon")
|
||||
export class HaServiceIcon extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property() public service?: string;
|
||||
|
||||
@property() public icon?: string;
|
||||
|
||||
@state()
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
protected _connection?: ContextType<typeof connectionContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
protected _config?: ContextType<typeof configContext>;
|
||||
|
||||
protected render() {
|
||||
if (this.icon) {
|
||||
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||
@@ -28,13 +35,13 @@ export class HaServiceIcon extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
if (!this.hass) {
|
||||
if (!this._connection || !this._config) {
|
||||
return this._renderFallback();
|
||||
}
|
||||
|
||||
const icon = serviceIcon(
|
||||
this.hass.connection,
|
||||
this.hass.config,
|
||||
this._connection.connection,
|
||||
this._config?.config,
|
||||
this.service
|
||||
).then((icn) => {
|
||||
if (icn) {
|
||||
|
||||
@@ -62,11 +62,7 @@ class HaServicePicker extends LitElement {
|
||||
index
|
||||
) => html`
|
||||
<ha-combo-box-item type="button" .borderTop=${index !== 0}>
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${item.id}
|
||||
></ha-service-icon>
|
||||
<ha-service-icon slot="start" .service=${item.id}></ha-service-icon>
|
||||
<span slot="headline">${item.primary}</span>
|
||||
<span slot="supporting-text">${item.secondary}</span>
|
||||
${item.service_id && this.showServiceId
|
||||
@@ -112,11 +108,7 @@ class HaServicePicker extends LitElement {
|
||||
service;
|
||||
|
||||
return html`
|
||||
<ha-service-icon
|
||||
slot="start"
|
||||
.hass=${this.hass}
|
||||
.service=${serviceId}
|
||||
></ha-service-icon>
|
||||
<ha-service-icon slot="start" .service=${serviceId}></ha-service-icon>
|
||||
<span slot="headline">${serviceName}</span>
|
||||
${this.showServiceId
|
||||
? html`<span slot="supporting-text" class="code"
|
||||
|
||||
@@ -99,8 +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);
|
||||
color: var(--ha-color-fill-primary-loud-resting);
|
||||
border-color: var(--ha-color-fill-primary-loud-resting);
|
||||
}
|
||||
|
||||
[part~="label"] {
|
||||
|
||||
@@ -451,7 +451,6 @@ export class HatScriptGraph extends LitElement {
|
||||
${node.action
|
||||
? html`<ha-service-icon
|
||||
slot="icon"
|
||||
.hass=${this.hass}
|
||||
.service=${node.action}
|
||||
></ha-service-icon>`
|
||||
: nothing}
|
||||
|
||||
@@ -653,6 +653,7 @@ export interface ActionSidebarConfig extends BaseSidebarConfig {
|
||||
disable: () => void;
|
||||
continueOnError: () => void;
|
||||
duplicate: () => void;
|
||||
convert: () => void;
|
||||
cut: () => void;
|
||||
copy: () => void;
|
||||
insertAfter: (value: Action | Action[]) => boolean;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
@@ -14,7 +15,6 @@ import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-background";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
|
||||
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
@@ -97,36 +97,38 @@ class PanelClimate extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
${this._searchParams.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div slot="title">${this.hass.localize("panel.climate")}</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar">
|
||||
${this._searchParams.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
</ha-top-app-bar-fixed>
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div class="main-title">${this.hass.localize("panel.climate")}</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -167,11 +169,78 @@ class PanelClimate extends LitElement {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
.header {
|
||||
background-color: var(--app-header-background-color);
|
||||
color: var(--app-header-text-color, white);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: calc(
|
||||
var(--ha-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-right,
|
||||
0px
|
||||
)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .header {
|
||||
width: calc(
|
||||
var(--ha-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-left,
|
||||
0px
|
||||
) - var(--safe-area-inset-right, 0px)
|
||||
);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
:host([scrolled]) .header {
|
||||
box-shadow: var(
|
||||
--bar-box-shadow,
|
||||
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
.toolbar {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding: 0px 12px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
box-sizing: border-box;
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
}
|
||||
:host([narrow]) .toolbar {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.main-title {
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.narrow .main-title {
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
}
|
||||
hui-view-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
padding-inline-end: var(--safe-area-inset-right);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
:host([narrow]) hui-view-container {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-inline-start: var(--safe-area-inset-left);
|
||||
}
|
||||
hui-view {
|
||||
flex: 1 1 100%;
|
||||
|
||||
@@ -185,19 +185,19 @@ class SupervisorAppInfo extends MobileAwareMixin(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">
|
||||
${this._currentAddon.logo
|
||||
? html`
|
||||
<img
|
||||
class="logo"
|
||||
alt=""
|
||||
src="/api/hassio/addons/${this._currentAddon.slug}/logo"
|
||||
/>
|
||||
`
|
||||
: nothing}
|
||||
${getAppDisplayName(
|
||||
this._currentAddon.name,
|
||||
this._currentAddon.stage
|
||||
@@ -239,7 +239,17 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
? html`<supervisor-apps-state
|
||||
.state=${this._currentAddon.state}
|
||||
></supervisor-apps-state>`
|
||||
: nothing}
|
||||
: 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>
|
||||
`}
|
||||
</div>
|
||||
|
||||
<ha-chip-set class="capabilities">
|
||||
@@ -503,8 +513,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
</div>
|
||||
${(this._currentAddon.update_available && this._updateEntityId) ||
|
||||
this._computeShowWebUI ||
|
||||
this._computeShowIngressUI ||
|
||||
!this._currentAddon.version
|
||||
this._computeShowIngressUI
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${this._currentAddon.update_available && this._updateEntityId
|
||||
@@ -540,19 +549,6 @@ class SupervisorAppInfo extends MobileAwareMixin(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}
|
||||
@@ -1501,17 +1497,16 @@ class SupervisorAppInfo extends MobileAwareMixin(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 {
|
||||
@@ -1530,15 +1525,17 @@ class SupervisorAppInfo extends MobileAwareMixin(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: 40px;
|
||||
max-height: 60px;
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-assist-chip {
|
||||
--md-sys-color-primary: var(--text-primary-color);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiHelpCircle } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { internationalizationContext } from "../../../../data/context";
|
||||
import type { AddonState } from "../../../../data/hassio/addon";
|
||||
|
||||
@@ -14,14 +16,13 @@ class SupervisorAppsState extends LitElement {
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
protected render(): TemplateResult {
|
||||
// Consider "unknown" state as "stopped" for display purposes
|
||||
// because unknown doesn't add any value to the user
|
||||
const displayState = this.state === "unknown" ? "stopped" : this.state;
|
||||
return html`
|
||||
<div class="dot state-${displayState}"></div>
|
||||
${this.state === "unknown"
|
||||
? html`<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>`
|
||||
: html` <div class="dot state-${this.state}"></div> `}
|
||||
<span
|
||||
>${this._i18n.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.state.${displayState}`
|
||||
`ui.panel.config.apps.dashboard.capability.state.${this.state}`
|
||||
)}</span
|
||||
>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
||||
import { computeObjectId } from "../../../../common/entity/compute_object_id";
|
||||
import {
|
||||
ACTION_BUILDING_BLOCKS,
|
||||
ACTION_COMBINED_BLOCKS,
|
||||
} from "../../../../data/action";
|
||||
import type { Action, ServiceAction } from "../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import { getAutomationActionType } from "./ha-automation-action-row";
|
||||
|
||||
type FieldSelector = Record<string, unknown> | undefined;
|
||||
|
||||
const getSelectorType = (selector: FieldSelector): string | undefined => {
|
||||
if (!selector) {
|
||||
return undefined;
|
||||
}
|
||||
const keys = Object.keys(selector);
|
||||
return keys.length === 1 ? keys[0] : undefined;
|
||||
};
|
||||
|
||||
const getSelectOptionValues = (
|
||||
selector: FieldSelector
|
||||
): string[] | undefined => {
|
||||
const config = selector?.select as
|
||||
| { options?: readonly (string | { value: string })[] }
|
||||
| null
|
||||
| undefined;
|
||||
if (!config?.options) {
|
||||
return undefined;
|
||||
}
|
||||
return config.options.map((opt) =>
|
||||
typeof opt === "string" ? opt : opt.value
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A value is compatible with the new field if either:
|
||||
* - the new field has no selector (unknown shape — accept),
|
||||
* - the old field had no selector but the new one does (best effort — accept),
|
||||
* - the selector types match. For `select` selectors, additionally require
|
||||
* that every supplied value is present in the new option list.
|
||||
*/
|
||||
const isFieldValueCompatible = (
|
||||
value: unknown,
|
||||
oldSelector: FieldSelector,
|
||||
newSelector: FieldSelector
|
||||
): boolean => {
|
||||
const newType = getSelectorType(newSelector);
|
||||
if (newType === undefined) {
|
||||
return true;
|
||||
}
|
||||
const oldType = getSelectorType(oldSelector);
|
||||
if (oldType !== undefined && oldType !== newType) {
|
||||
return false;
|
||||
}
|
||||
if (newType === "select") {
|
||||
const allowed = getSelectOptionValues(newSelector);
|
||||
if (!allowed) {
|
||||
return true;
|
||||
}
|
||||
const allowedSet = new Set(allowed);
|
||||
const values = Array.isArray(value) ? value : [value];
|
||||
return values.every((v) => typeof v === "string" && allowedSet.has(v));
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const filterTargetEntitiesByDomain = (
|
||||
target: HassServiceTarget,
|
||||
domain: string | undefined
|
||||
): HassServiceTarget => {
|
||||
if (!domain || target.entity_id === undefined) {
|
||||
return target;
|
||||
}
|
||||
const entityIds = Array.isArray(target.entity_id)
|
||||
? target.entity_id
|
||||
: [target.entity_id];
|
||||
const filtered = entityIds.filter((id) => computeDomain(id) === domain);
|
||||
const { entity_id: _entityId, ...rest } = target;
|
||||
if (filtered.length === 0) {
|
||||
return rest;
|
||||
}
|
||||
return { ...rest, entity_id: filtered };
|
||||
};
|
||||
|
||||
export const BASE_ACTION_FIELDS = [
|
||||
"alias",
|
||||
"note",
|
||||
"enabled",
|
||||
"continue_on_error",
|
||||
] as const;
|
||||
|
||||
const BUILDING_BLOCK_TYPES = new Set<string>([
|
||||
...ACTION_BUILDING_BLOCKS,
|
||||
...ACTION_COMBINED_BLOCKS,
|
||||
]);
|
||||
|
||||
export const isBuildingBlockAction = (action: Action): boolean => {
|
||||
const type = getAutomationActionType(action);
|
||||
return type !== undefined && BUILDING_BLOCK_TYPES.has(type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Encode an action as a stable picker key.
|
||||
* - Service actions → `domain.service` (always contains a dot).
|
||||
* - Everything else → the action type identifier (no dot).
|
||||
*/
|
||||
export const getActionKey = (action: Action): string | undefined => {
|
||||
const type = getAutomationActionType(action);
|
||||
if (type === "service") {
|
||||
return (action as ServiceAction).action || undefined;
|
||||
}
|
||||
return type;
|
||||
};
|
||||
|
||||
const isServiceKey = (key: string) => key.includes(".");
|
||||
|
||||
/** Build a fresh action with default content for the given picker key. */
|
||||
export const buildActionFromKey = (key: string): Action => {
|
||||
if (isServiceKey(key)) {
|
||||
return {
|
||||
action: key,
|
||||
metadata: {},
|
||||
} as ServiceAction;
|
||||
}
|
||||
const elClass = customElements.get(`ha-automation-action-${key}`) as
|
||||
| (CustomElementConstructor & { defaultConfig?: Action })
|
||||
| undefined;
|
||||
if (elClass?.defaultConfig) {
|
||||
return { ...elClass.defaultConfig };
|
||||
}
|
||||
return { [key]: {} } as Action;
|
||||
};
|
||||
|
||||
const isServiceAction = (action: Action): action is ServiceAction =>
|
||||
"action" in action && !!(action as ServiceAction).action;
|
||||
|
||||
/**
|
||||
* Merge fields from `oldAction` into `newAction` that are still compatible.
|
||||
* Behavior: copy base fields if old has them, and for service → service copy
|
||||
* `target` and any `data` keys that the new service still supports.
|
||||
*/
|
||||
export const convertAction = (
|
||||
oldAction: Action,
|
||||
newAction: Action,
|
||||
services: HomeAssistant["services"]
|
||||
): Action => {
|
||||
const merged: Action = { ...newAction };
|
||||
|
||||
for (const field of BASE_ACTION_FIELDS) {
|
||||
const oldValue = (oldAction as Record<string, unknown>)[field];
|
||||
if (oldValue !== undefined) {
|
||||
(merged as Record<string, unknown>)[field] = oldValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (isServiceAction(oldAction) && isServiceAction(merged)) {
|
||||
if (oldAction.target) {
|
||||
const newDomain = merged.action
|
||||
? computeDomain(merged.action)
|
||||
: undefined;
|
||||
merged.target = filterTargetEntitiesByDomain(oldAction.target, newDomain);
|
||||
}
|
||||
|
||||
if (oldAction.data && merged.action && oldAction.action) {
|
||||
const newDomain = computeDomain(merged.action);
|
||||
const newService = computeObjectId(merged.action);
|
||||
const newFields = services[newDomain]?.[newService]?.fields;
|
||||
const oldDomain = computeDomain(oldAction.action);
|
||||
const oldService = computeObjectId(oldAction.action);
|
||||
const oldFields = services[oldDomain]?.[oldService]?.fields;
|
||||
if (newFields) {
|
||||
const carried: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(oldAction.data)) {
|
||||
const newField = newFields[key];
|
||||
if (
|
||||
newField &&
|
||||
isFieldValueCompatible(
|
||||
value,
|
||||
oldFields?.[key]?.selector,
|
||||
newField.selector
|
||||
)
|
||||
) {
|
||||
carried[key] = value;
|
||||
}
|
||||
}
|
||||
if (Object.keys(carried).length > 0) {
|
||||
merged.data = carried;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return merged;
|
||||
};
|
||||
@@ -0,0 +1,400 @@
|
||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
|
||||
import { consume, type ContextType } from "@lit/context";
|
||||
import { mdiArrowRight, mdiRoomService } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-button";
|
||||
import "../../../../components/ha-combo-box-item";
|
||||
import "../../../../components/ha-dialog";
|
||||
import "../../../../components/ha-dialog-footer";
|
||||
import "../../../../components/ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../../../../components/ha-picker-combo-box";
|
||||
import type { PickerValueRenderer } from "../../../../components/ha-picker-field";
|
||||
import "../../../../components/ha-service-icon";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import "../../../../components/radio/ha-radio-group";
|
||||
import "../../../../components/radio/ha-radio-option";
|
||||
import {
|
||||
ACTION_BUILDING_BLOCKS,
|
||||
ACTION_COMBINED_BLOCKS,
|
||||
ACTION_ICONS,
|
||||
YAML_ONLY_ACTION_TYPES,
|
||||
} from "../../../../data/action";
|
||||
import {
|
||||
internationalizationContext,
|
||||
servicesContext,
|
||||
} from "../../../../data/context";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import { DialogMixin } from "../../../../dialogs/dialog-mixin";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import {
|
||||
buildActionFromKey,
|
||||
convertAction,
|
||||
getActionKey,
|
||||
} from "./convert-action";
|
||||
import type { ConvertActionDialogParams } from "./show-dialog-convert-action";
|
||||
|
||||
interface ConvertItem extends PickerComboBoxItem {
|
||||
isService: boolean;
|
||||
}
|
||||
|
||||
const EXCLUDED_TYPES = new Set<string>([
|
||||
...ACTION_BUILDING_BLOCKS,
|
||||
...ACTION_COMBINED_BLOCKS,
|
||||
...YAML_ONLY_ACTION_TYPES,
|
||||
// Generic catch-all action types covered by the services list below
|
||||
"service",
|
||||
// Umbrella repeat — not a leaf type
|
||||
"repeat",
|
||||
]);
|
||||
|
||||
@customElement("dialog-convert-action")
|
||||
class DialogConvertAction extends DialogMixin<ConvertActionDialogParams>(
|
||||
LitElement
|
||||
) {
|
||||
@state() private _pickedKey?: string;
|
||||
|
||||
@state() private _mode: "current" | "new" = "current";
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
|
||||
@state()
|
||||
@consume({ context: servicesContext, subscribe: true })
|
||||
private _services!: ContextType<typeof servicesContext>;
|
||||
|
||||
public connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
if (this.params) {
|
||||
this._pickedKey = getActionKey(this.params.currentAction);
|
||||
this._mode = "current";
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
if (!this.params) {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const currentKey = getActionKey(this.params.currentAction);
|
||||
const canCommit =
|
||||
this._pickedKey !== undefined && this._pickedKey !== currentKey;
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
open
|
||||
header-title=${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.title"
|
||||
)}
|
||||
>
|
||||
<div class="content">
|
||||
${currentKey
|
||||
? html`
|
||||
<div class="preview">
|
||||
${this._renderActionChip(currentKey)}
|
||||
<ha-svg-icon
|
||||
class="arrow"
|
||||
.path=${mdiArrowRight}
|
||||
></ha-svg-icon>
|
||||
${this._pickedKey && this._pickedKey !== currentKey
|
||||
? this._renderActionChip(this._pickedKey)
|
||||
: "?"}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
<ha-generic-picker
|
||||
required
|
||||
.value=${this._pickedKey}
|
||||
.getItems=${this._getItems}
|
||||
.rowRenderer=${this._rowRenderer}
|
||||
.valueRenderer=${this._valueRenderer(
|
||||
this._i18n.localize,
|
||||
this._services
|
||||
)}
|
||||
.notFoundLabel=${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.no_matches"
|
||||
)}
|
||||
@value-changed=${this._pickedKeyChanged}
|
||||
></ha-generic-picker>
|
||||
<ha-alert alert-type="warning">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.warning"
|
||||
)}
|
||||
</ha-alert>
|
||||
<ha-radio-group
|
||||
name="mode"
|
||||
.value=${this._mode}
|
||||
@change=${this._modeChanged}
|
||||
>
|
||||
<ha-radio-option value="current">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.mode_current"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
<ha-radio-option value="new">
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.mode_new"
|
||||
)}
|
||||
</ha-radio-option>
|
||||
</ha-radio-group>
|
||||
</div>
|
||||
<ha-dialog-footer slot="footer">
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
appearance="plain"
|
||||
@click=${this.closeDialog}
|
||||
>
|
||||
${this._i18n.localize("ui.common.cancel")}
|
||||
</ha-button>
|
||||
<ha-button
|
||||
slot="primaryAction"
|
||||
.disabled=${!canCommit}
|
||||
@click=${this._convert}
|
||||
>
|
||||
${this._i18n.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert_dialog.convert"
|
||||
)}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
private _items = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"]
|
||||
): ConvertItem[] => {
|
||||
const items: ConvertItem[] = [];
|
||||
|
||||
for (const type of Object.keys(ACTION_ICONS)) {
|
||||
if (EXCLUDED_TYPES.has(type)) {
|
||||
continue;
|
||||
}
|
||||
const label =
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${type}.label` as any
|
||||
) || type;
|
||||
items.push({
|
||||
id: type,
|
||||
primary: label,
|
||||
icon_path: ACTION_ICONS[type as keyof typeof ACTION_ICONS],
|
||||
isService: false,
|
||||
sorting_label: `0_${label}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (services) {
|
||||
for (const domain of Object.keys(services)) {
|
||||
const domainName = domainToName(localize, domain);
|
||||
for (const service of Object.keys(services[domain])) {
|
||||
const serviceId = `${domain}.${service}`;
|
||||
const def = services[domain][service];
|
||||
const serviceName =
|
||||
localize(
|
||||
`component.${domain}.services.${service}.name` as any,
|
||||
def.description_placeholders
|
||||
) ||
|
||||
def.name ||
|
||||
service;
|
||||
const description =
|
||||
localize(
|
||||
`component.${domain}.services.${service}.description` as any,
|
||||
def.description_placeholders
|
||||
) ||
|
||||
def.description ||
|
||||
"";
|
||||
items.push({
|
||||
id: serviceId,
|
||||
primary: `${domainName}: ${serviceName}`,
|
||||
secondary: description,
|
||||
isService: true,
|
||||
search_labels: {
|
||||
serviceId,
|
||||
domainName,
|
||||
serviceName,
|
||||
description,
|
||||
},
|
||||
sorting_label: `1_${domainName}_${serviceName}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
);
|
||||
|
||||
private _getItems = () => this._items(this._i18n.localize, this._services);
|
||||
|
||||
private _rowRenderer: RenderItemFunction<PickerComboBoxItem> = (
|
||||
item,
|
||||
index
|
||||
) => {
|
||||
const convertItem = item as ConvertItem;
|
||||
return html`
|
||||
<ha-combo-box-item type="button" .borderTop=${index !== 0}>
|
||||
${convertItem.isService
|
||||
? html`<ha-service-icon
|
||||
slot="start"
|
||||
.service=${item.id}
|
||||
></ha-service-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${item.icon_path}
|
||||
></ha-svg-icon>`}
|
||||
<span slot="headline">${item.primary}</span>
|
||||
${item.secondary
|
||||
? html`<span slot="supporting-text">${item.secondary}</span>`
|
||||
: nothing}
|
||||
</ha-combo-box-item>
|
||||
`;
|
||||
};
|
||||
|
||||
private _valueRenderer = memoizeOne(
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
services: HomeAssistant["services"]
|
||||
): PickerValueRenderer =>
|
||||
(value) => {
|
||||
if (value.includes(".")) {
|
||||
const [domain, service] = value.split(".");
|
||||
const def = services?.[domain]?.[service];
|
||||
const domainName = domainToName(localize, domain);
|
||||
const serviceName =
|
||||
localize(
|
||||
`component.${domain}.services.${service}.name` as any,
|
||||
def?.description_placeholders
|
||||
) ||
|
||||
def?.name ||
|
||||
service;
|
||||
return html`
|
||||
<ha-service-icon slot="start" .service=${value}></ha-service-icon>
|
||||
<span slot="headline">${domainName}: ${serviceName}</span>
|
||||
`;
|
||||
}
|
||||
const iconPath = ACTION_ICONS[value as keyof typeof ACTION_ICONS];
|
||||
const label =
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${value}.label` as any
|
||||
) || value;
|
||||
return html`
|
||||
${iconPath
|
||||
? html`<ha-svg-icon slot="start" .path=${iconPath}></ha-svg-icon>`
|
||||
: html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiRoomService}
|
||||
></ha-svg-icon>`}
|
||||
<span slot="headline">${label}</span>
|
||||
`;
|
||||
}
|
||||
);
|
||||
|
||||
private _renderActionChip(key: string) {
|
||||
const localize = this._i18n.localize;
|
||||
if (key.includes(".")) {
|
||||
const [domain, service] = key.split(".");
|
||||
const def = this._services?.[domain]?.[service];
|
||||
const domainName = domainToName(localize, domain);
|
||||
const serviceName =
|
||||
localize(
|
||||
`component.${domain}.services.${service}.name` as any,
|
||||
def?.description_placeholders
|
||||
) ||
|
||||
def?.name ||
|
||||
service;
|
||||
return html`
|
||||
<div class="chip">
|
||||
<ha-service-icon .service=${key}></ha-service-icon>
|
||||
<span>${domainName}: ${serviceName}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
const iconPath = ACTION_ICONS[key as keyof typeof ACTION_ICONS];
|
||||
const label =
|
||||
localize(
|
||||
`ui.panel.config.automation.editor.actions.type.${key}.label` as any
|
||||
) || key;
|
||||
return html`
|
||||
<div class="chip">
|
||||
<ha-svg-icon .path=${iconPath || mdiRoomService}></ha-svg-icon>
|
||||
<span>${label}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private _pickedKeyChanged = (ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
this._pickedKey = ev.detail?.value || undefined;
|
||||
};
|
||||
|
||||
private _modeChanged = (ev: Event) => {
|
||||
const value = (ev.target as HTMLInputElement).value;
|
||||
if (value === "current" || value === "new") {
|
||||
this._mode = value;
|
||||
}
|
||||
};
|
||||
|
||||
private _convert = () => {
|
||||
if (!this.params || !this._pickedKey) {
|
||||
return;
|
||||
}
|
||||
const newAction = buildActionFromKey(this._pickedKey);
|
||||
const merged = convertAction(
|
||||
this.params.currentAction,
|
||||
newAction,
|
||||
this._services
|
||||
);
|
||||
if (this._mode === "new") {
|
||||
this.params.duplicateConvert(merged);
|
||||
} else {
|
||||
this.params.convert(merged);
|
||||
}
|
||||
this.closeDialog();
|
||||
};
|
||||
|
||||
static styles = css`
|
||||
ha-dialog {
|
||||
--mdc-dialog-min-width: min(560px, 100vw);
|
||||
}
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ha-space-3);
|
||||
}
|
||||
.preview {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.preview .chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
padding: var(--ha-space-1) var(--ha-space-3);
|
||||
border-radius: 999px;
|
||||
background-color: var(--secondary-background-color);
|
||||
min-width: 0;
|
||||
}
|
||||
.preview .chip span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.preview .arrow {
|
||||
color: var(--secondary-text-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"dialog-convert-action": DialogConvertAction;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
mdiPlusCircleMultipleOutline,
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
mdiSwapHorizontal,
|
||||
} from "@mdi/js";
|
||||
import deepClone from "deep-clone-simple";
|
||||
import type { HassServiceTarget } from "home-assistant-js-websocket";
|
||||
@@ -90,8 +91,10 @@ import { showEditorToast } from "../editor-toast";
|
||||
import "../ha-automation-editor-warning";
|
||||
import { overflowStyles, rowStyles } from "../styles";
|
||||
import "../target/ha-automation-row-targets";
|
||||
import { isBuildingBlockAction } from "./convert-action";
|
||||
import "./ha-automation-action-editor";
|
||||
import type HaAutomationActionEditor from "./ha-automation-action-editor";
|
||||
import { showConvertActionDialog } from "./show-dialog-convert-action";
|
||||
import "./types/ha-automation-action-choose";
|
||||
import "./types/ha-automation-action-condition";
|
||||
import "./types/ha-automation-action-delay";
|
||||
@@ -308,7 +311,6 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
<ha-service-icon
|
||||
slot="leading-icon"
|
||||
class="action-icon"
|
||||
.hass=${this.hass}
|
||||
.service=${this.action.action}
|
||||
></ha-service-icon>
|
||||
`
|
||||
@@ -334,15 +336,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
? this._renderTargets(
|
||||
target,
|
||||
actionHasTarget && !this._isNew,
|
||||
serviceTargetSpec,
|
||||
type !== "device_id"
|
||||
serviceTargetSpec
|
||||
)
|
||||
: nothing}
|
||||
${noteTooltipText
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
@@ -522,6 +522,16 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
></ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${!isBuildingBlockAction(this.action)
|
||||
? html`<ha-dropdown-item value="convert" .disabled=${this.disabled}>
|
||||
<ha-svg-icon slot="icon" .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
${this._renderOverflowLabel(
|
||||
this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert"
|
||||
)
|
||||
)}
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-dropdown-item
|
||||
value="toggle_yaml_mode"
|
||||
@@ -723,14 +733,13 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
targetSpec?: TargetSelector["target"]
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
@@ -980,6 +989,24 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
fireEvent(this, "duplicate");
|
||||
};
|
||||
|
||||
private _convertAction = () => {
|
||||
showConvertActionDialog(this, {
|
||||
currentAction: this.action,
|
||||
convert: (newAction) => {
|
||||
fireEvent(this, "value-changed", { value: newAction });
|
||||
|
||||
if (this._selected && this.optionsInSidebar) {
|
||||
this.openSidebar(newAction);
|
||||
} else if (this._yamlMode) {
|
||||
this._actionEditor?.yamlEditor?.setValue(newAction);
|
||||
}
|
||||
},
|
||||
duplicateConvert: (newAction) => {
|
||||
this._insertAfter(newAction);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
private _insertAfter = (value: Action | Action[]) => {
|
||||
if (ensureArray(value).some((val) => !isAction(val))) {
|
||||
return false;
|
||||
@@ -1105,6 +1132,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
paste: this._pasteAction,
|
||||
pasteAvailable: this._pasteAvailable,
|
||||
duplicate: this._duplicateAction,
|
||||
convert: this._convertAction,
|
||||
insertAfter: this._insertAfter,
|
||||
run: this._runAction,
|
||||
config: {
|
||||
@@ -1209,6 +1237,9 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
case "move_down":
|
||||
this._moveDown();
|
||||
break;
|
||||
case "convert":
|
||||
this._convertAction();
|
||||
break;
|
||||
case "toggle_yaml_mode":
|
||||
this._toggleYamlMode(ev.target as HTMLElement);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { Action } from "../../../../data/script";
|
||||
|
||||
export interface ConvertActionDialogParams {
|
||||
currentAction: Action;
|
||||
convert: (newAction: Action) => void;
|
||||
duplicateConvert: (newAction: Action) => void;
|
||||
}
|
||||
|
||||
const loadDialog = () => import("./dialog-convert-action");
|
||||
|
||||
export const showConvertActionDialog = (
|
||||
element: HTMLElement,
|
||||
dialogParams: ConvertActionDialogParams
|
||||
): void => {
|
||||
fireEvent(element, "show-dialog", {
|
||||
dialogTag: "dialog-convert-action",
|
||||
dialogImport: loadDialog,
|
||||
dialogParams,
|
||||
});
|
||||
};
|
||||
@@ -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,
|
||||
PASTE_VALUE,
|
||||
getAddAutomationElementTargetFromQuery,
|
||||
PASTE_VALUE,
|
||||
} from "./show-add-automation-element-dialog";
|
||||
import { getTargetText } from "./target/get_target_text";
|
||||
|
||||
@@ -795,33 +795,37 @@ class DialogAddAutomationElement
|
||||
class="paste"
|
||||
@click=${this._paste}
|
||||
>
|
||||
<div slot="headline" class="label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.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>
|
||||
<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}
|
||||
@@ -1634,7 +1638,6 @@ class DialogAddAutomationElement
|
||||
result.push({
|
||||
icon: html`
|
||||
<ha-service-icon
|
||||
.hass=${this.hass}
|
||||
.service=${`${dmn}.${service}`}
|
||||
></ha-service-icon>
|
||||
`,
|
||||
@@ -1953,7 +1956,6 @@ class DialogAddAutomationElement
|
||||
items[domain].items.push({
|
||||
icon: html`
|
||||
<ha-service-icon
|
||||
.hass=${this.hass}
|
||||
.service=${`${domain}.${serviceName}`}
|
||||
></ha-service-icon>
|
||||
`,
|
||||
@@ -2542,16 +2544,23 @@ class DialogAddAutomationElement
|
||||
ha-svg-icon.plus {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.shortcut {
|
||||
.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 {
|
||||
--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 span {
|
||||
.shortcut-label .shortcut span {
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-family: var(--ha-font-family-code);
|
||||
color: var(--ha-color-text-secondary);
|
||||
|
||||
@@ -224,15 +224,13 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
? this._renderTargets(
|
||||
target,
|
||||
descriptionHasTarget && !this._isNew,
|
||||
conditionTargetSpec,
|
||||
this.condition.condition !== "device"
|
||||
conditionTargetSpec
|
||||
)
|
||||
: nothing}
|
||||
${this.condition.note?.trim()
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
@@ -575,14 +573,13 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
targetSpec?: TargetSelector["target"]
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
|
||||
@@ -161,7 +161,6 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
mdiPlusCircleMultipleOutline,
|
||||
mdiRenameBox,
|
||||
mdiStopCircleOutline,
|
||||
mdiSwapHorizontal,
|
||||
} from "@mdi/js";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
@@ -27,6 +28,7 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
|
||||
import "../../../../components/ha-dropdown-item";
|
||||
import { ACTION_BUILDING_BLOCKS } from "../../../../data/action";
|
||||
import type { ActionSidebarConfig } from "../../../../data/automation";
|
||||
import { isBuildingBlockAction } from "../action/convert-action";
|
||||
import type { DomainManifestLookup } from "../../../../data/integration";
|
||||
import { domainToName } from "../../../../data/integration";
|
||||
import type {
|
||||
@@ -285,6 +287,22 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
</ha-dropdown-item>
|
||||
`
|
||||
: nothing}
|
||||
${!isBuildingBlockAction(this.config.config.action)
|
||||
? html`<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="convert"
|
||||
.disabled=${this.disabled}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${mdiSwapHorizontal}></ha-svg-icon>
|
||||
<div class="overflow-label">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.actions.convert"
|
||||
)}
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
|
||||
<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
value="toggle_yaml_mode"
|
||||
@@ -451,6 +469,9 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
case "duplicate":
|
||||
this.config.duplicate();
|
||||
break;
|
||||
case "convert":
|
||||
this.config.convert();
|
||||
break;
|
||||
case "copy":
|
||||
this.config.copy();
|
||||
break;
|
||||
|
||||
@@ -60,9 +60,6 @@ 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>;
|
||||
@@ -92,12 +89,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
private _countCache = new Map<
|
||||
string,
|
||||
Promise<number | undefined> | number | undefined
|
||||
>();
|
||||
|
||||
private _rerenderCount = true;
|
||||
private _countCache = new Map<string, Promise<number | undefined>>();
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
@@ -106,15 +98,10 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
changedProps.has("selector") ||
|
||||
changedProps.has("_registries")
|
||||
) {
|
||||
this._rerenderCount = true;
|
||||
this._countCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
this._rerenderCount = false;
|
||||
}
|
||||
|
||||
private _countMatchingEntities(referencedEntities: string[]): number {
|
||||
const targetSelector = this.selector;
|
||||
const hasEntityFilter = !!targetSelector?.target?.entity;
|
||||
@@ -161,11 +148,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetId: string
|
||||
) {
|
||||
const key = `${targetType}:${targetId}`;
|
||||
let fallback = " (-)";
|
||||
if (!this._countCache.has(key) || this._rerenderCount) {
|
||||
if (typeof this._countCache.get(key) === "number") {
|
||||
fallback = ` (${this._countCache.get(key)})`;
|
||||
}
|
||||
if (!this._countCache.has(key)) {
|
||||
this._countCache.set(
|
||||
key,
|
||||
extractFromTarget(
|
||||
@@ -179,30 +162,15 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
.then((result) =>
|
||||
this._countMatchingEntities(result.referenced_entities)
|
||||
)
|
||||
.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error counting target entities", err);
|
||||
return undefined;
|
||||
})
|
||||
.catch(() => undefined)
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
return until(
|
||||
this._countCache
|
||||
.get(key)!
|
||||
.then((count) => (count === undefined ? nothing : html` (${count})`)),
|
||||
"(-)"
|
||||
);
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -281,9 +249,8 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleTargetSelect}
|
||||
@click=${stopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
>
|
||||
<button slot="trigger" class="target">
|
||||
<span slot="trigger" class="target interactive">
|
||||
<ha-svg-icon .path=${mdiFormatListBulleted}></ha-svg-icon>
|
||||
<div class="label">
|
||||
${this._i18n.localize(
|
||||
@@ -294,7 +261,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</button>
|
||||
</span>
|
||||
${rows.map(([targetType, targetId]) => {
|
||||
const content = html`${lastTargetType !== null &&
|
||||
lastTargetType !== targetType
|
||||
@@ -349,37 +316,21 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetType?: string,
|
||||
countTemplate: unknown = nothing
|
||||
) {
|
||||
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
|
||||
return html`<div
|
||||
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>
|
||||
</button>`;
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _renderTarget(
|
||||
@@ -433,7 +384,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetId,
|
||||
this._getLabel
|
||||
);
|
||||
if (targetType !== "entity" && this.interactive) {
|
||||
if (targetType !== "entity") {
|
||||
countTemplate = this._renderCount(targetType, targetId);
|
||||
}
|
||||
}
|
||||
@@ -493,13 +444,6 @@ 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;
|
||||
@@ -589,10 +533,10 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
button.target {
|
||||
.target.interactive {
|
||||
cursor: pointer;
|
||||
}
|
||||
button.target:hover {
|
||||
.target.interactive:hover {
|
||||
background: var(--ha-color-fill-neutral-normal-hover);
|
||||
}
|
||||
|
||||
|
||||
@@ -249,15 +249,13 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
? this._renderTargets(
|
||||
target,
|
||||
descriptionHasTarget && !this._isNew,
|
||||
triggerTargetSpec,
|
||||
type !== "device"
|
||||
triggerTargetSpec
|
||||
)
|
||||
: nothing}
|
||||
${type !== "list" &&
|
||||
(this.trigger as Exclude<Trigger, TriggerList>).note?.trim()
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
tabindex="0"
|
||||
id="note-icon"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
@@ -559,14 +557,13 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
targetSpec?: TargetSelector["target"]
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ export class CloudRegister extends LitElement {
|
||||
|
||||
try {
|
||||
await cloudRegister(this.hass, email, password);
|
||||
this._verificationEmailSent(email, "account_created");
|
||||
this._verificationEmailSent(email);
|
||||
} catch (err: any) {
|
||||
this._password = "";
|
||||
this._requestInProgress = false;
|
||||
@@ -238,18 +238,15 @@ export class CloudRegister extends LitElement {
|
||||
|
||||
const email = emailField.value || "";
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
const doResend = async (username: string) => {
|
||||
try {
|
||||
await cloudResendVerification(this.hass, username);
|
||||
this._verificationEmailSent(username, "verification_email_sent");
|
||||
this._verificationEmailSent(username);
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doResend(username.toLowerCase());
|
||||
} else {
|
||||
this._requestInProgress = false;
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
@@ -261,16 +258,13 @@ export class CloudRegister extends LitElement {
|
||||
await doResend(email);
|
||||
}
|
||||
|
||||
private _verificationEmailSent(
|
||||
email: string,
|
||||
messageKey: "account_created" | "verification_email_sent"
|
||||
) {
|
||||
private _verificationEmailSent(email: string) {
|
||||
this._requestInProgress = false;
|
||||
this._password = "";
|
||||
fireEvent(this, "cloud-email-changed", { value: email });
|
||||
fireEvent(this, "cloud-done", {
|
||||
flashMessage: this.hass.localize(
|
||||
`ui.panel.config.cloud.register.${messageKey}`
|
||||
"ui.panel.config.cloud.register.account_created"
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -330,48 +330,42 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
return rssi > -33 ? 3 : rssi > -66 ? 2 : 1;
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams) => {
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
const { dataType, data } = params as CallbackDataParams;
|
||||
let tooltipText = "";
|
||||
if (dataType === "edge") {
|
||||
const { source, target, value } = data as any;
|
||||
const sourceName = this._getBluetoothDeviceName(source);
|
||||
const targetName = this._getBluetoothDeviceName(target);
|
||||
return html`${sourceName} →
|
||||
${targetName}${source !== CORE_SOURCE_ID
|
||||
? html` <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b>
|
||||
${value}`
|
||||
: nothing}`;
|
||||
tooltipText = `${sourceName} → ${targetName}`;
|
||||
if (source !== CORE_SOURCE_ID) {
|
||||
tooltipText += ` <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${value}`;
|
||||
}
|
||||
} else {
|
||||
const { id: address } = data as any;
|
||||
const name = this._getBluetoothDeviceName(address);
|
||||
const btDevice = this._data.find((d) => d.address === address);
|
||||
if (btDevice) {
|
||||
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}<br><b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b> ${btDevice.rssi}<br><b>${this.hass.localize("ui.panel.config.bluetooth.source")}:</b> ${btDevice.source}<br><b>${this.hass.localize("ui.panel.config.bluetooth.updated")}:</b> ${relativeTime(new Date(btDevice.time * 1000), this.hass.locale)}`;
|
||||
const device = this._sourceDevices[address];
|
||||
if (device) {
|
||||
const area = getDeviceArea(device, this.hass.areas);
|
||||
if (area) {
|
||||
tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const device = this._sourceDevices[address];
|
||||
if (device) {
|
||||
tooltipText = `<b>${name}</b><br><b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b> ${address}`;
|
||||
const area = getDeviceArea(device, this.hass.areas);
|
||||
if (area) {
|
||||
tooltipText += `<br><b>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b>${area.name}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const { id: address } = data as any;
|
||||
const name = this._getBluetoothDeviceName(address);
|
||||
const btDevice = this._data.find((d) => d.address === address);
|
||||
const device = this._sourceDevices[address];
|
||||
const area = device ? getDeviceArea(device, this.hass.areas) : undefined;
|
||||
const areaLine = area
|
||||
? html`<br /><b
|
||||
>${this.hass.localize("ui.panel.config.bluetooth.area")}: </b
|
||||
>${area.name}`
|
||||
: nothing;
|
||||
if (btDevice) {
|
||||
return html`<b>${name}</b><br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b>
|
||||
${address}<br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b>
|
||||
${btDevice.rssi}<br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.source")}:</b>
|
||||
${btDevice.source}<br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.updated")}:</b>
|
||||
${relativeTime(
|
||||
new Date(btDevice.time * 1000),
|
||||
this.hass.locale
|
||||
)}${areaLine}`;
|
||||
}
|
||||
if (device) {
|
||||
return html`<b>${name}</b><br />
|
||||
<b>${this.hass.localize("ui.panel.config.bluetooth.address")}:</b>
|
||||
${address}${areaLine}`;
|
||||
}
|
||||
return nothing;
|
||||
return tooltipText;
|
||||
};
|
||||
|
||||
private _handleChartClick(e: CustomEvent): void {
|
||||
|
||||
@@ -131,7 +131,7 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
this._searchFilter = (ev.target as HaInputSearch).value ?? "";
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams) => {
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
const { dataType, data, name } = params as CallbackDataParams;
|
||||
if (dataType === "edge") {
|
||||
const { source, target, value } = data as any;
|
||||
@@ -141,45 +141,40 @@ export class ZHANetworkVisualizationPage extends LitElement {
|
||||
const sourceName = this._networkData.nodes.find(
|
||||
(node) => node.id === source
|
||||
)!.name;
|
||||
const tooltipText = `${sourceName} → ${targetName}${value ? ` <b>LQI:</b> ${value}` : ""}`;
|
||||
|
||||
const reverseValue = this._networkData.links.find(
|
||||
(link) => link.source === source && link.target === target
|
||||
)?.reverseValue;
|
||||
return html`${sourceName} →
|
||||
${targetName}${value
|
||||
? html` <b>LQI:</b> ${value}`
|
||||
: nothing}${reverseValue
|
||||
? html`<br />${targetName} → ${sourceName} <b>LQI:</b> ${reverseValue}`
|
||||
: nothing}`;
|
||||
if (reverseValue) {
|
||||
return `${tooltipText}<br>${targetName} → ${sourceName} <b>LQI:</b> ${reverseValue}`;
|
||||
}
|
||||
return tooltipText;
|
||||
}
|
||||
const device = this._devices.find((d) => d.ieee === (data as any).id);
|
||||
if (!device) {
|
||||
return html`${name}`;
|
||||
return name;
|
||||
}
|
||||
let label = `<b>IEEE: </b>${device.ieee}`;
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.device_type")}: </b>${device.device_type.replace("_", " ")}`;
|
||||
if (device.nwk != null) {
|
||||
label += `<br><b>NWK: </b>${formatAsPaddedHex(device.nwk)}`;
|
||||
}
|
||||
if (device.manufacturer != null && device.model != null) {
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.device")}: </b>${device.manufacturer} ${device.model}`;
|
||||
} else {
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.device_not_in_db")}</b>`;
|
||||
}
|
||||
const haDevice = this.hass.devices[device.device_reg_id] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
const area = haDevice
|
||||
? getDeviceArea(haDevice, this.hass.areas)
|
||||
: undefined;
|
||||
return html`<b>IEEE: </b>${device.ieee}<br /><b
|
||||
>${this.hass.localize("ui.panel.config.zha.visualization.device_type")}: </b
|
||||
>${device.device_type.replace("_", " ")}${device.nwk != null
|
||||
? html`<br /><b>NWK: </b>${formatAsPaddedHex(device.nwk)}`
|
||||
: nothing}${device.manufacturer != null && device.model != null
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.device"
|
||||
)}: </b
|
||||
>${device.manufacturer} ${device.model}`
|
||||
: html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zha.visualization.device_not_in_db"
|
||||
)}</b
|
||||
>`}${area
|
||||
? html`<br /><b
|
||||
>${this.hass.localize("ui.panel.config.zha.visualization.area")}: </b
|
||||
>${area.name}`
|
||||
: nothing}`;
|
||||
if (haDevice) {
|
||||
const area = getDeviceArea(haDevice, this.hass.areas);
|
||||
if (area) {
|
||||
label += `<br><b>${this.hass.localize("ui.panel.config.zha.visualization.area")}: </b>${area.name}`;
|
||||
}
|
||||
}
|
||||
return label;
|
||||
};
|
||||
|
||||
private async _refreshTopology(): Promise<void> {
|
||||
|
||||
@@ -9,7 +9,6 @@ import { getDeviceArea } from "../../../../../common/entity/context/get_device_c
|
||||
import { navigate } from "../../../../../common/navigate";
|
||||
import { debounce } from "../../../../../common/util/debounce";
|
||||
import "../../../../../components/chart/ha-network-graph";
|
||||
import "../../../../../components/chart/ha-chart-tooltip-marker";
|
||||
import type {
|
||||
NetworkData,
|
||||
NetworkLink,
|
||||
@@ -151,7 +150,7 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
this._searchFilter = (ev.target as HaInputSearch).value ?? "";
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams) => {
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
const { dataType, data } = params as CallbackDataParams;
|
||||
if (dataType === "edge") {
|
||||
const { source, target, value } = data as any;
|
||||
@@ -161,66 +160,39 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
sourceDevice?.name_by_user ?? sourceDevice?.name ?? source;
|
||||
const targetName =
|
||||
targetDevice?.name_by_user ?? targetDevice?.name ?? target;
|
||||
let tip = `${sourceName} → ${targetName}`;
|
||||
const route =
|
||||
this._nodeStatistics[source]?.lwr || this._nodeStatistics[source]?.nlwr;
|
||||
return html`${sourceName} →
|
||||
${targetName}${route?.protocol_data_rate
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.data_rate"
|
||||
)}:</b
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.protocol_data_rate.${route.protocol_data_rate}` as any
|
||||
)}`
|
||||
: nothing}${value ? html`<br /><b>RSSI:</b> ${value}` : nothing}`;
|
||||
if (route?.protocol_data_rate) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.data_rate")}:</b> ${this.hass.localize(`ui.panel.config.zwave_js.protocol_data_rate.${route.protocol_data_rate}`)}`;
|
||||
}
|
||||
if (value) {
|
||||
tip += `<br><b>RSSI:</b> ${value}`;
|
||||
}
|
||||
return tip;
|
||||
}
|
||||
const { id, name } = data as any;
|
||||
const device = this._devices[id] as DeviceRegistryEntry | undefined;
|
||||
const nodeStatus = this._nodeStatuses[id];
|
||||
const area = device ? getDeviceArea(device, this.hass.areas) : undefined;
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String((params as CallbackDataParams).color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${name}<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.node_id"
|
||||
)}:</b
|
||||
>
|
||||
${id}${device
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.manufacturer"
|
||||
)}:</b
|
||||
>
|
||||
${device.manufacturer || "-"}<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.model"
|
||||
)}:</b
|
||||
>
|
||||
${device.model || "-"}`
|
||||
: nothing}${nodeStatus
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.status"
|
||||
)}:</b
|
||||
>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.zwave_js.node_status.${nodeStatus.status}` as any
|
||||
)}${nodeStatus.zwave_plus_version
|
||||
? html`<br /><b>Z-Wave Plus:</b> ${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.version"
|
||||
)}
|
||||
${nodeStatus.zwave_plus_version}`
|
||||
: nothing}`
|
||||
: nothing}${area
|
||||
? html`<br /><b
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.zwave_js.visualization.area"
|
||||
)}:</b
|
||||
>
|
||||
${area.name}`
|
||||
: nothing}`;
|
||||
let tip = `${(params as any).marker} ${name}`;
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.node_id")}:</b> ${id}`;
|
||||
if (device) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.manufacturer")}:</b> ${device.manufacturer || "-"}`;
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.model")}:</b> ${device.model || "-"}`;
|
||||
}
|
||||
if (nodeStatus) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.status")}:</b> ${this.hass.localize(`ui.panel.config.zwave_js.node_status.${nodeStatus.status}`)}`;
|
||||
if (nodeStatus.zwave_plus_version) {
|
||||
tip += `<br><b>Z-Wave Plus:</b> ${this.hass.localize("ui.panel.config.zwave_js.visualization.version")} ${nodeStatus.zwave_plus_version}`;
|
||||
}
|
||||
}
|
||||
if (device) {
|
||||
const area = getDeviceArea(device, this.hass.areas);
|
||||
if (area) {
|
||||
tip += `<br><b>${this.hass.localize("ui.panel.config.zwave_js.visualization.area")}:</b> ${area.name}`;
|
||||
}
|
||||
}
|
||||
return tip;
|
||||
};
|
||||
|
||||
private _getNetworkData = memoizeOne(
|
||||
|
||||
@@ -158,7 +158,6 @@ export default class HaScriptFieldRow extends LitElement {
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { goBack } from "../../common/navigate";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
@@ -14,7 +15,6 @@ import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
import "../lovelace/views/hui-view-background";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
|
||||
const LIGHT_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
@@ -97,36 +97,38 @@ class PanelLight extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-top-app-bar-fixed .narrow=${this.narrow}>
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div slot="title">${this.hass.localize("panel.light")}</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
<div class="header ${classMap({ narrow: this.narrow })}">
|
||||
<div class="toolbar">
|
||||
${this._searchParms.has("historyBack")
|
||||
? html`
|
||||
<ha-icon-button-arrow-prev
|
||||
@click=${this._back}
|
||||
slot="navigationIcon"
|
||||
></ha-icon-button-arrow-prev>
|
||||
`
|
||||
: html`
|
||||
<ha-menu-button
|
||||
slot="navigationIcon"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
</ha-top-app-bar-fixed>
|
||||
></ha-menu-button>
|
||||
`}
|
||||
<div class="main-title">${this.hass.localize("panel.light")}</div>
|
||||
</div>
|
||||
</div>
|
||||
${this._lovelace
|
||||
? html`
|
||||
<hui-view-container .hass=${this.hass}>
|
||||
<hui-view-background .hass=${this.hass}> </hui-view-background>
|
||||
<hui-view
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -167,11 +169,78 @@ class PanelLight extends LitElement {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
.header {
|
||||
background-color: var(--app-header-background-color);
|
||||
color: var(--app-header-text-color, white);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: calc(
|
||||
var(--ha-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-right,
|
||||
0px
|
||||
)
|
||||
);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
z-index: 4;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
-webkit-backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
backdrop-filter: var(--app-header-backdrop-filter, none);
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
}
|
||||
:host([narrow]) .header {
|
||||
width: calc(
|
||||
var(--ha-top-app-bar-width, 100%) - var(
|
||||
--safe-area-inset-left,
|
||||
0px
|
||||
) - var(--safe-area-inset-right, 0px)
|
||||
);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
}
|
||||
:host([scrolled]) .header {
|
||||
box-shadow: var(
|
||||
--bar-box-shadow,
|
||||
0px 2px 4px -1px rgba(0, 0, 0, 0.2),
|
||||
0px 4px 5px 0px rgba(0, 0, 0, 0.14),
|
||||
0px 1px 10px 0px rgba(0, 0, 0, 0.12)
|
||||
);
|
||||
}
|
||||
.toolbar {
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
flex: 1;
|
||||
align-items: center;
|
||||
font-size: var(--ha-font-size-xl);
|
||||
padding: 0px 12px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
box-sizing: border-box;
|
||||
border-bottom: var(--app-header-border-bottom, none);
|
||||
}
|
||||
:host([narrow]) .toolbar {
|
||||
padding: 0 4px;
|
||||
}
|
||||
.main-title {
|
||||
margin-inline-start: var(--ha-space-6);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
flex-grow: 1;
|
||||
}
|
||||
.narrow .main-title {
|
||||
margin-inline-start: var(--ha-space-2);
|
||||
}
|
||||
hui-view-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding-top: calc(var(--header-height) + var(--safe-area-inset-top));
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
padding-inline-end: var(--safe-area-inset-right);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
:host([narrow]) hui-view-container {
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-inline-start: var(--safe-area-inset-left);
|
||||
}
|
||||
hui-view {
|
||||
flex: 1 1 100%;
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, nothing } from "lit";
|
||||
import {
|
||||
subHours,
|
||||
differenceInDays,
|
||||
@@ -33,10 +31,10 @@ import {
|
||||
formatDateWeekdayVeryShortDate,
|
||||
} from "../../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../../common/datetime/format_time";
|
||||
import type { HaECOption } from "../../../../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../../../../resources/echarts/echarts";
|
||||
import { filterXSS } from "../../../../../common/util/xss";
|
||||
import type { StatisticPeriod } from "../../../../../data/recorder";
|
||||
import { getPeriodicAxisLabelConfig } from "../../../../../components/chart/axis-label";
|
||||
import "../../../../../components/chart/ha-chart-tooltip-marker";
|
||||
import { getSuggestedPeriod } from "../../../../../data/energy";
|
||||
|
||||
export { fillDataGapsAndRoundCaps } from "../../../../../components/chart/round-caps";
|
||||
@@ -112,7 +110,7 @@ export function getCommonOptions(
|
||||
formatTotal?: (total: number) => string,
|
||||
detailedDailyData = false,
|
||||
yAxisFractionDigits = 1
|
||||
): HaECOption {
|
||||
): ECOption {
|
||||
const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData);
|
||||
let suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
|
||||
|
||||
@@ -136,7 +134,7 @@ export function getCommonOptions(
|
||||
}
|
||||
}
|
||||
|
||||
const monthTimeAxis: HaECOption = {
|
||||
const monthTimeAxis: ECOption = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: subDays(start, MONTH_TIME_AXIS_PADDING),
|
||||
@@ -148,7 +146,7 @@ export function getCommonOptions(
|
||||
splitNumber: Math.min(differenceInCalendarMonths(end, start), 5),
|
||||
},
|
||||
};
|
||||
const normalTimeAxis: HaECOption = {
|
||||
const normalTimeAxis: ECOption = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: start,
|
||||
@@ -156,7 +154,7 @@ export function getCommonOptions(
|
||||
},
|
||||
};
|
||||
|
||||
const options: HaECOption = {
|
||||
const options: ECOption = {
|
||||
...(suggestedPeriod === "month" ? monthTimeAxis : normalTimeAxis),
|
||||
yAxis: {
|
||||
type: "value",
|
||||
@@ -181,7 +179,7 @@ export function getCommonOptions(
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
formatter: (params: TopLevelFormatterParams) => {
|
||||
formatter: (params: TopLevelFormatterParams): string => {
|
||||
// trigger: "axis" gives an array of params, but "item" gives a single param
|
||||
if (Array.isArray(params)) {
|
||||
const mainItems: CallbackDataParams[] = [];
|
||||
@@ -193,7 +191,7 @@ export function getCommonOptions(
|
||||
mainItems.push(param);
|
||||
}
|
||||
});
|
||||
const sections = [mainItems, compareItems]
|
||||
return [mainItems, compareItems]
|
||||
.map((items) =>
|
||||
formatTooltip(
|
||||
items,
|
||||
@@ -206,12 +204,8 @@ export function getCommonOptions(
|
||||
formatTotal
|
||||
)
|
||||
)
|
||||
.filter((s): s is TemplateResult => s !== nothing);
|
||||
if (sections.length === 0) return nothing;
|
||||
return html`${sections.map(
|
||||
(section, i) =>
|
||||
html`${i > 0 ? html`<br /><br />` : nothing}${section}`
|
||||
)}`;
|
||||
.filter(Boolean)
|
||||
.join("<br><br>");
|
||||
}
|
||||
return formatTooltip(
|
||||
[params],
|
||||
@@ -238,9 +232,9 @@ function formatTooltip(
|
||||
showCompareYear: boolean,
|
||||
unit?: string,
|
||||
formatTotal?: (total: number) => string
|
||||
): TemplateResult | typeof nothing {
|
||||
) {
|
||||
if (!params[0]?.value) {
|
||||
return nothing;
|
||||
return "";
|
||||
}
|
||||
// displayX may be shifted from the period start (see EnergyDataPoint);
|
||||
// originalStart has the real date for display. Gap-filled entries lack it.
|
||||
@@ -264,50 +258,43 @@ function formatTooltip(
|
||||
period += ` – ${formatTime(addHours(date, 1), locale, config)}`;
|
||||
}
|
||||
}
|
||||
const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`;
|
||||
|
||||
let sumPositive = 0;
|
||||
let countPositive = 0;
|
||||
let sumNegative = 0;
|
||||
let countNegative = 0;
|
||||
const rows: TemplateResult[] = [];
|
||||
for (const param of params) {
|
||||
const y = param.value?.[1] as number;
|
||||
const value = formatNumber(
|
||||
y,
|
||||
locale,
|
||||
y < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||
);
|
||||
if (value === "0") {
|
||||
continue;
|
||||
}
|
||||
if (param.componentSubType === "bar") {
|
||||
if (y > 0) {
|
||||
sumPositive += y;
|
||||
countPositive++;
|
||||
} else {
|
||||
sumNegative += y;
|
||||
countNegative++;
|
||||
const values = params
|
||||
.map((param) => {
|
||||
const y = param.value?.[1] as number;
|
||||
const value = formatNumber(
|
||||
y,
|
||||
locale,
|
||||
y < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||
);
|
||||
if (value === "0") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
rows.push(
|
||||
html`<ha-chart-tooltip-marker
|
||||
.color=${String(param.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${param.seriesName}:
|
||||
<div style="direction:ltr; display: inline;">${value} ${unit}</div>`
|
||||
);
|
||||
if (param.componentSubType === "bar") {
|
||||
if (y > 0) {
|
||||
sumPositive += y;
|
||||
countPositive++;
|
||||
} else {
|
||||
sumNegative += y;
|
||||
countNegative++;
|
||||
}
|
||||
}
|
||||
return `${param.marker} ${filterXSS(param.seriesName!)}: <div style="direction:ltr; display: inline;">${value} ${unit}</div>`;
|
||||
})
|
||||
.filter(Boolean);
|
||||
let footer = "";
|
||||
if (sumPositive !== 0 && countPositive > 1 && formatTotal) {
|
||||
footer += `<br><b>${formatTotal(sumPositive)}</b>`;
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
return nothing;
|
||||
if (sumNegative !== 0 && countNegative > 1 && formatTotal) {
|
||||
footer += `<br><b>${formatTotal(sumNegative)}</b>`;
|
||||
}
|
||||
return html`<h4 style="text-align: center; margin: 0;">${period}</h4>
|
||||
${rows.map(
|
||||
(row, i) => html`${i > 0 ? html`<br />` : nothing}${row}`
|
||||
)}${sumPositive !== 0 && countPositive > 1 && formatTotal
|
||||
? html`<br /><b>${formatTotal(sumPositive)}</b>`
|
||||
: nothing}${sumNegative !== 0 && countNegative > 1 && formatTotal
|
||||
? html`<br /><b>${formatTotal(sumNegative)}</b>`
|
||||
: nothing}`;
|
||||
return values.length > 0 ? `${title}${values.join("<br>")}${footer}` : "";
|
||||
}
|
||||
|
||||
function getDatapointX(datapoint: NonNullable<LineSeriesOption["data"]>[0]) {
|
||||
|
||||
@@ -44,7 +44,7 @@ import {
|
||||
getCompareTransform,
|
||||
} from "./common/energy-chart-options";
|
||||
import { storage } from "../../../../common/decorators/storage";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
|
||||
|
||||
@@ -216,7 +216,7 @@ export class HuiEnergyDevicesDetailGraphCard
|
||||
compareStart: Date | undefined,
|
||||
compareEnd: Date | undefined,
|
||||
yAxisFractionDigits: number
|
||||
): HaECOption => {
|
||||
): ECOption => {
|
||||
const commonOptions = getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
|
||||
@@ -9,10 +9,10 @@ import type { BarSeriesOption, PieSeriesOption } from "echarts/charts";
|
||||
import { PieChart } from "echarts/charts";
|
||||
import type { ECElementEvent } from "echarts/types/dist/shared";
|
||||
import type { PieDataItemOption } from "echarts/types/src/chart/pie/PieSeries";
|
||||
import { filterXSS } from "../../../../common/util/xss";
|
||||
import { getGraphColorByIndex } from "../../../../common/color/colors";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
import "../../../../components/chart/ha-chart-tooltip-marker";
|
||||
import type { EnergyData } from "../../../../data/energy";
|
||||
import {
|
||||
computeConsumptionData,
|
||||
@@ -30,7 +30,7 @@ import type { HomeAssistant } from "../../../../types";
|
||||
import type { LovelaceCard } from "../../types";
|
||||
import type { EnergyDevicesGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import "../../../../components/ha-card";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { measureTextWidth } from "../../../../util/text";
|
||||
@@ -198,28 +198,24 @@ export class HuiEnergyDevicesGraphCard
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const deviceName = this._getDeviceName(params.name);
|
||||
private _renderTooltip(params: any) {
|
||||
const deviceName = filterXSS(this._getDeviceName(params.name));
|
||||
const title = `<h4 style="text-align: center; margin: 0;">${deviceName}</h4>`;
|
||||
const value = `${formatNumber(
|
||||
params.value[0] as number,
|
||||
this.hass.locale,
|
||||
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||
)} kWh ${params.percent ? `(${params.percent} %)` : ""}`;
|
||||
return html`<h4 style="text-align: center; margin: 0;">${deviceName}</h4>
|
||||
<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${params.seriesName}:
|
||||
<div style="direction:ltr; display: inline;">${value}</div>`;
|
||||
};
|
||||
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
(
|
||||
data: (BarSeriesOption | PieSeriesOption)[],
|
||||
chartType: "bar" | "pie",
|
||||
legendData: typeof this._legendData
|
||||
): HaECOption => {
|
||||
const options: HaECOption = {
|
||||
): ECOption => {
|
||||
const options: ECOption = {
|
||||
grid: {
|
||||
top: 5,
|
||||
left: 5,
|
||||
@@ -229,7 +225,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
formatter: this._renderTooltip,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
},
|
||||
xAxis: { show: false },
|
||||
yAxis: { show: false },
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
} from "./common/energy-chart-options";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import "./common/hui-energy-graph-chip";
|
||||
import "../../../../components/ha-tooltip";
|
||||
|
||||
@@ -177,7 +177,7 @@ export class HuiEnergyGasGraphCard
|
||||
compareStart: Date | undefined,
|
||||
compareEnd: Date | undefined,
|
||||
yAxisFractionDigits: number
|
||||
): HaECOption =>
|
||||
): ECOption =>
|
||||
getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
} from "./common/energy-chart-options";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import "./common/hui-energy-graph-chip";
|
||||
import "../../../../components/ha-tooltip";
|
||||
|
||||
@@ -65,7 +65,7 @@ export class HuiEnergySolarGraphCard
|
||||
};
|
||||
}
|
||||
|
||||
@state() private _chartData: (BarSeriesOption | LineSeriesOption)[] = [];
|
||||
@state() private _chartData: ECOption["series"][] = [];
|
||||
|
||||
@state() private _yAxisFractionDigits = 1;
|
||||
|
||||
@@ -175,7 +175,7 @@ export class HuiEnergySolarGraphCard
|
||||
compareStart: Date | undefined,
|
||||
compareEnd: Date | undefined,
|
||||
yAxisFractionDigits: number
|
||||
): HaECOption =>
|
||||
): ECOption =>
|
||||
getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
@@ -213,7 +213,7 @@ export class HuiEnergySolarGraphCard
|
||||
}
|
||||
}
|
||||
|
||||
const datasets: (BarSeriesOption | LineSeriesOption)[] = [];
|
||||
const datasets: ECOption["series"] = [];
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
|
||||
@@ -6,7 +6,10 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { BarSeriesOption } from "echarts/charts";
|
||||
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
|
||||
import type {
|
||||
TooltipOption,
|
||||
TopLevelFormatterParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { getEnergyColor } from "./common/color";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
@@ -40,7 +43,7 @@ import {
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
} from "./common/energy-chart-options";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
|
||||
const colorPropertyMap = {
|
||||
to_grid: "--energy-grid-return-color",
|
||||
@@ -193,7 +196,7 @@ export class HuiEnergyUsageGraphCard
|
||||
compareStart: Date | undefined,
|
||||
compareEnd: Date | undefined,
|
||||
yAxisFractionDigits: number
|
||||
): HaECOption => {
|
||||
): ECOption => {
|
||||
const commonOptions = getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
@@ -206,22 +209,15 @@ export class HuiEnergyUsageGraphCard
|
||||
false,
|
||||
yAxisFractionDigits
|
||||
);
|
||||
const tooltip = commonOptions.tooltip;
|
||||
const baseFormatter =
|
||||
tooltip &&
|
||||
!Array.isArray(tooltip) &&
|
||||
typeof tooltip.formatter === "function"
|
||||
? tooltip.formatter
|
||||
: undefined;
|
||||
const options: HaECOption = {
|
||||
const options: ECOption = {
|
||||
...commonOptions,
|
||||
tooltip: {
|
||||
...commonOptions.tooltip,
|
||||
formatter: (params: TopLevelFormatterParams) => {
|
||||
formatter: (params: TopLevelFormatterParams): string => {
|
||||
if (!Array.isArray(params)) {
|
||||
return nothing;
|
||||
return "";
|
||||
}
|
||||
const sorted = [...params].sort((a, b) => {
|
||||
params.sort((a, b) => {
|
||||
const aValue = (a.value as number[])?.[1];
|
||||
const bValue = (b.value as number[])?.[1];
|
||||
if (aValue > 0 && bValue < 0) {
|
||||
@@ -235,7 +231,9 @@ export class HuiEnergyUsageGraphCard
|
||||
}
|
||||
return a.componentIndex - b.componentIndex;
|
||||
});
|
||||
return baseFormatter ? baseFormatter(sorted) : nothing;
|
||||
return (
|
||||
(commonOptions.tooltip as TooltipOption)?.formatter as any
|
||||
)?.(params);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
} from "./common/energy-chart-options";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "./common/hui-energy-graph-chip";
|
||||
import "../../../../components/ha-tooltip";
|
||||
@@ -177,7 +177,7 @@ export class HuiEnergyWaterGraphCard
|
||||
compareStart: Date | undefined,
|
||||
compareEnd: Date | undefined,
|
||||
yAxisFractionDigits: number
|
||||
): HaECOption =>
|
||||
): ECOption =>
|
||||
getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
|
||||
@@ -24,7 +24,7 @@ import type { LovelaceCard } from "../../types";
|
||||
import type { PowerSourcesGraphCardConfig } from "../types";
|
||||
import { hasConfigChanged } from "../../common/has-changed";
|
||||
import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import { hex2rgb } from "../../../../common/color/convert-color";
|
||||
import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base";
|
||||
|
||||
@@ -148,7 +148,7 @@ export class HuiPowerSourcesGraphCard
|
||||
compareEnd: Date | undefined,
|
||||
legendData: CustomLegendOption["data"] | undefined,
|
||||
yAxisFractionDigits: number
|
||||
): HaECOption => ({
|
||||
): ECOption => ({
|
||||
...getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiAlertCircle, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { ConditionListenersController } from "../../../../common/controllers/condition-listeners-controller";
|
||||
import "../../../../components/ha-alert";
|
||||
import "../../../../components/ha-svg-icon";
|
||||
import { HaRowItem } from "../../../../components/item/ha-row-item";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
@@ -29,15 +28,17 @@ const STATE_ICONS: Record<VisibilityState, string> = {
|
||||
|
||||
/**
|
||||
* @element ha-visibility-status
|
||||
* @extends {HaRowItem}
|
||||
*
|
||||
* @summary
|
||||
* Alert banner that surfaces the live visibility result for a set of
|
||||
* lovelace conditions.
|
||||
* Row-style banner that surfaces the live visibility result for a set of
|
||||
* lovelace conditions. Replaces the static explanation alert at the top of
|
||||
* card / section / badge / conditional-card visibility editors.
|
||||
*
|
||||
* @attr {"visible"|"hidden"|"invalid"} state - Computed visibility state
|
||||
* @attr {"visible"|"hidden"|"invalid"} state - Computed visibility state (reflected for styling).
|
||||
*/
|
||||
@customElement("ha-visibility-status")
|
||||
export class HaVisibilityStatus extends LitElement {
|
||||
export class HaVisibilityStatus extends HaRowItem {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -47,7 +48,7 @@ export class HaVisibilityStatus extends LitElement {
|
||||
@consume({ context: conditionsEntityContext, subscribe: true })
|
||||
private _entityContext?: ConditionsEntityContext;
|
||||
|
||||
@property()
|
||||
@property({ reflect: true })
|
||||
public state: VisibilityState = "visible";
|
||||
|
||||
private _listeners = new ConditionListenersController(this);
|
||||
@@ -70,27 +71,23 @@ export class HaVisibilityStatus extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
protected override _renderInner(): TemplateResult {
|
||||
return html`
|
||||
<ha-alert
|
||||
.alertType=${this.state === "visible"
|
||||
? "success"
|
||||
: this.state === "hidden"
|
||||
? "warning"
|
||||
: "error"}
|
||||
>
|
||||
<ha-svg-icon slot="icon" .path=${STATE_ICONS[this.state]}></ha-svg-icon>
|
||||
<div class="headline">
|
||||
<div part="start" class="start">
|
||||
<ha-svg-icon .path=${STATE_ICONS[this.state]}></ha-svg-icon>
|
||||
</div>
|
||||
<div part="content" class="content">
|
||||
<div part="headline" class="headline">
|
||||
${this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.headline`
|
||||
)}
|
||||
</div>
|
||||
<div class="supporting">
|
||||
<div part="supporting-text" class="supporting">
|
||||
${this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.supporting${(this.conditions?.length ?? 0) === 0 ? "_empty" : ""}`
|
||||
)}
|
||||
</div>
|
||||
</ha-alert>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -120,13 +117,37 @@ export class HaVisibilityStatus extends LitElement {
|
||||
static styles: CSSResultGroup = [
|
||||
HaRowItem.styles,
|
||||
css`
|
||||
ha-alert {
|
||||
:host {
|
||||
display: block;
|
||||
border-radius: var(--ha-border-radius-xl);
|
||||
transition: background-color var(--ha-animation-duration-normal)
|
||||
ease-in-out;
|
||||
}
|
||||
.base {
|
||||
padding: var(--ha-space-4);
|
||||
}
|
||||
:host([state="visible"]) {
|
||||
background-color: var(--ha-color-fill-success-quiet-resting);
|
||||
--visibility-status-color: var(--ha-color-on-success-normal);
|
||||
}
|
||||
:host([state="hidden"]) {
|
||||
background-color: var(--ha-color-fill-warning-quiet-resting);
|
||||
--visibility-status-color: var(--ha-color-on-warning-normal);
|
||||
}
|
||||
:host([state="invalid"]) {
|
||||
background-color: var(--ha-color-fill-danger-quiet-resting);
|
||||
--visibility-status-color: var(--ha-color-on-danger-normal);
|
||||
}
|
||||
.start {
|
||||
align-self: start;
|
||||
}
|
||||
.start ha-svg-icon {
|
||||
color: var(--visibility-status-color);
|
||||
--mdc-icon-size: 24px;
|
||||
}
|
||||
.headline {
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
margin-bottom: var(--ha-space-1);
|
||||
white-space: normal;
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -67,14 +67,6 @@ export type ECOption = ComposeOption<
|
||||
| SunburstSeriesOption
|
||||
>;
|
||||
|
||||
export type {
|
||||
HaECOption,
|
||||
HaECSeries,
|
||||
HaECSeriesItem,
|
||||
HaTooltipOption,
|
||||
LitTooltipFormatter,
|
||||
} from "./ha-ec-option";
|
||||
|
||||
// Register the required components
|
||||
echarts.use([
|
||||
BarChart,
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import type { TemplateResult, nothing } from "lit";
|
||||
import type { TooltipOption } from "echarts/types/dist/shared";
|
||||
import type { ECOption } from "./echarts";
|
||||
|
||||
export type LitTooltipFormatter<P = any> = (
|
||||
params: P,
|
||||
ticket?: string
|
||||
) => TemplateResult | typeof nothing | null | undefined;
|
||||
|
||||
export type HaTooltipOption = Omit<TooltipOption, "formatter"> & {
|
||||
formatter?: string | LitTooltipFormatter;
|
||||
};
|
||||
|
||||
type RawSeriesOption = Exclude<
|
||||
NonNullable<ECOption["series"]>,
|
||||
readonly unknown[]
|
||||
>;
|
||||
|
||||
/** Single series item with optional Lit tooltip formatter */
|
||||
export type HaECSeriesItem = Omit<RawSeriesOption, "tooltip"> & {
|
||||
tooltip?: HaTooltipOption;
|
||||
};
|
||||
|
||||
/** Series array passed to ha-chart-base `.data` */
|
||||
export type HaECSeries = HaECSeriesItem[];
|
||||
|
||||
export type HaECOption = {
|
||||
[K in keyof ECOption]: K extends "tooltip"
|
||||
? HaTooltipOption | HaTooltipOption[] | undefined
|
||||
: K extends "series"
|
||||
? HaECSeriesItem | HaECSeriesItem[] | undefined
|
||||
: ECOption[K];
|
||||
};
|
||||
@@ -5648,6 +5648,15 @@
|
||||
"run_action_error": "Error running action",
|
||||
"run_action_success": "Action ran successfully",
|
||||
"duplicate": "[%key:ui::common::duplicate%]",
|
||||
"convert": "Convert to…",
|
||||
"convert_dialog": {
|
||||
"title": "Convert action",
|
||||
"warning": "Compatible settings are carried over; anything else is removed. You can undo if you change your mind.",
|
||||
"convert": "Convert",
|
||||
"mode_current": "Convert current action",
|
||||
"mode_new": "Convert into a new action",
|
||||
"no_matches": "No matching actions"
|
||||
},
|
||||
"re_order": "[%key:ui::panel::config::automation::editor::triggers::re_order%]",
|
||||
"rename": "[%key:ui::panel::config::automation::editor::triggers::rename%]",
|
||||
"cut": "[%key:ui::panel::config::automation::editor::triggers::cut%]",
|
||||
@@ -6319,8 +6328,7 @@
|
||||
"resend_confirm_email": "Resend confirmation email",
|
||||
"clicked_confirm": "I opened the confirmation link",
|
||||
"confirm_email": "Check the email we just sent to {email} and open the confirmation link to continue.",
|
||||
"account_created": "Account created! Check your email for instructions on how to activate your account.",
|
||||
"verification_email_sent": "Verification email sent. Check your inbox for instructions on how to activate your account."
|
||||
"account_created": "Account created! Check your email for instructions on how to activate your account."
|
||||
},
|
||||
"account": {
|
||||
"download_support_package": "Download support package",
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { html, nothing } from "lit";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { LitTooltipFormatter } from "../../../src/resources/echarts/echarts";
|
||||
import { wrapLitTooltipFormatter } from "../../../src/components/chart/lit-tooltip-formatter";
|
||||
|
||||
describe("wrapLitTooltipFormatter", () => {
|
||||
it("renders TemplateResult into a stable container", () => {
|
||||
const formatter = () => html`<b>Hello</b>`;
|
||||
const wrapped = wrapLitTooltipFormatter(formatter);
|
||||
const first = wrapped({});
|
||||
const second = wrapped({});
|
||||
|
||||
expect(first).toBe(second);
|
||||
expect(first?.tagName).toBe("DIV");
|
||||
expect(first?.style.display).toBe("contents");
|
||||
expect(first?.textContent).toContain("Hello");
|
||||
});
|
||||
|
||||
it("returns null for nothing, null, and undefined", () => {
|
||||
const returnNothing: LitTooltipFormatter = () => nothing;
|
||||
expect(wrapLitTooltipFormatter(returnNothing)({})).toBeNull();
|
||||
expect(wrapLitTooltipFormatter(() => null)({})).toBeNull();
|
||||
expect(wrapLitTooltipFormatter(() => undefined)({})).toBeNull();
|
||||
});
|
||||
|
||||
it("returns the same wrapped function for the same formatter", () => {
|
||||
const formatter = () => html`x`;
|
||||
expect(wrapLitTooltipFormatter(formatter)).toBe(
|
||||
wrapLitTooltipFormatter(formatter)
|
||||
);
|
||||
});
|
||||
|
||||
it("does not double-wrap an already wrapped formatter", () => {
|
||||
const formatter = () => html`x`;
|
||||
const wrapped = wrapLitTooltipFormatter(formatter);
|
||||
expect(wrapLitTooltipFormatter(wrapped)).toBe(wrapped);
|
||||
});
|
||||
});
|
||||