Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 64f03ef147 | |||
| e373689a37 | |||
| 5edcdb8977 | |||
| 26b8921e8c | |||
| b8c201b6d3 | |||
| 4a6c23c93e | |||
| e2712cb0b0 | |||
| db52cd0d8e | |||
| 4891783c86 | |||
| b73732acdb | |||
| d950514104 | |||
| f37cf1e848 | |||
| a188ef1b7a | |||
| 087ef159df |
@@ -27,7 +27,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "7.29.2",
|
||||
"@babel/runtime": "7.29.7",
|
||||
"@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.0",
|
||||
"@babel/core": "7.29.7",
|
||||
"@babel/helper-define-polyfill-provider": "0.6.8",
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.5",
|
||||
"@babel/plugin-transform-runtime": "7.29.7",
|
||||
"@babel/preset-env": "7.29.7",
|
||||
"@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.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -14,6 +14,7 @@ import type {
|
||||
ECElementEvent,
|
||||
LegendComponentOption,
|
||||
LineSeriesOption,
|
||||
TooltipOption,
|
||||
XAXisOption,
|
||||
YAXisOption,
|
||||
} from "echarts/types/dist/shared";
|
||||
@@ -29,22 +30,59 @@ import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import { afterNextRender } from "../../common/util/render-status";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { uiContext } from "../../data/context";
|
||||
import type { Themes } from "../../data/ws-themes";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type {
|
||||
ECOption,
|
||||
HaECOption,
|
||||
HaECSeries,
|
||||
HaECSeriesItem,
|
||||
HaTooltipOption,
|
||||
} from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant, HomeAssistantUI } from "../../types";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import "../chips/ha-assist-chip";
|
||||
import "../ha-icon-button";
|
||||
import { formatTimeLabel } from "./axis-label";
|
||||
import { downSampleLineData } from "./down-sample";
|
||||
import { wrapLitTooltipFormatter } from "./lit-tooltip-formatter";
|
||||
|
||||
export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
|
||||
const LEGEND_OVERFLOW_LIMIT = 10;
|
||||
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
|
||||
const DOUBLE_TAP_TIME = 300;
|
||||
|
||||
type RawSeriesOption = Exclude<
|
||||
NonNullable<ECOption["series"]>,
|
||||
readonly unknown[]
|
||||
>;
|
||||
|
||||
const toEChartsFormatter = (
|
||||
fn: ReturnType<typeof wrapLitTooltipFormatter>
|
||||
): NonNullable<TooltipOption["formatter"]> =>
|
||||
fn as NonNullable<TooltipOption["formatter"]>;
|
||||
|
||||
const convertHaTooltipFormatter = (tooltip: HaTooltipOption): TooltipOption => {
|
||||
const { formatter, ...rest } = tooltip;
|
||||
const next: TooltipOption = { ...rest };
|
||||
if (typeof formatter === "function") {
|
||||
next.formatter = toEChartsFormatter(wrapLitTooltipFormatter(formatter));
|
||||
} else if (formatter !== undefined) {
|
||||
next.formatter = formatter;
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const processSeriesTooltipFormatter = (s: HaECSeriesItem): RawSeriesOption => {
|
||||
if (s.tooltip && typeof s.tooltip.formatter === "function") {
|
||||
return {
|
||||
...s,
|
||||
tooltip: convertHaTooltipFormatter(s.tooltip),
|
||||
} as RawSeriesOption;
|
||||
}
|
||||
return s as RawSeriesOption;
|
||||
};
|
||||
|
||||
export type CustomLegendOption = ECOption["legend"] & {
|
||||
type: "custom";
|
||||
data?: {
|
||||
@@ -66,9 +104,9 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public data: ECOption["series"] = [];
|
||||
@property({ attribute: false }) public data: HaECSeries = [];
|
||||
|
||||
@property({ attribute: false }) public options?: ECOption;
|
||||
@property({ attribute: false }) public options?: HaECOption;
|
||||
|
||||
@property({ type: String }) public height?: string;
|
||||
|
||||
@@ -614,7 +652,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
// Return an array of all IDs associated with the legend item of the primaryId
|
||||
private _getAllIdsFromLegend(
|
||||
options: ECOption | undefined,
|
||||
options: HaECOption | undefined,
|
||||
primaryId: string
|
||||
): string[] {
|
||||
if (!options) return [primaryId];
|
||||
@@ -634,7 +672,7 @@ export class HaChartBase extends LitElement {
|
||||
|
||||
// Parses the options structure and adds all ids of unselected legend items to hiddenDatasets.
|
||||
// No known need to remove items at this time.
|
||||
private _updateHiddenStatsFromOptions(options: ECOption | undefined) {
|
||||
private _updateHiddenStatsFromOptions(options: HaECOption | undefined) {
|
||||
if (!options) return;
|
||||
const legend = ensureArray(this.options?.legend || [])[0] as
|
||||
| LegendComponentOption
|
||||
@@ -757,22 +795,34 @@ export class HaChartBase extends LitElement {
|
||||
xAxis,
|
||||
};
|
||||
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
if (isMobile && options.tooltip) {
|
||||
// mobile charts are full width so we need to confine the tooltip to the chart
|
||||
const tooltips = Array.isArray(options.tooltip)
|
||||
? options.tooltip
|
||||
: [options.tooltip];
|
||||
tooltips.forEach((tooltip) => {
|
||||
tooltip.confine = true;
|
||||
tooltip.appendTo = undefined;
|
||||
tooltip.triggerOn = "click";
|
||||
});
|
||||
options.tooltip = tooltips;
|
||||
if (options.tooltip) {
|
||||
const isMobile = window.matchMedia(
|
||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||
).matches;
|
||||
// Shallow-copy each tooltip object so wrap/mobile mutations don't leak
|
||||
// back into the caller's options.tooltip reference (callers may cache the
|
||||
// options object via memoizeOne, in which case in-place mutation would
|
||||
// pollute that cache across chart instances).
|
||||
const processTooltip = (tooltip: HaTooltipOption): TooltipOption => {
|
||||
const next = convertHaTooltipFormatter(tooltip);
|
||||
if (isMobile) {
|
||||
// mobile charts are full width so we need to confine the tooltip to the chart
|
||||
next.confine = true;
|
||||
next.appendTo = undefined;
|
||||
next.triggerOn = "click";
|
||||
}
|
||||
return next;
|
||||
};
|
||||
const haTooltip = options.tooltip;
|
||||
const processedTooltip = Array.isArray(haTooltip)
|
||||
? haTooltip.map(processTooltip)
|
||||
: processTooltip(haTooltip);
|
||||
return {
|
||||
...options,
|
||||
tooltip: processedTooltip,
|
||||
} as ECOption;
|
||||
}
|
||||
return options;
|
||||
return options as ECOption;
|
||||
}
|
||||
|
||||
private _createTheme(style: CSSStyleDeclaration) {
|
||||
@@ -960,8 +1010,12 @@ 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.sampling === "minmax") {
|
||||
if ((s as LineSeriesOption).sampling === "minmax") {
|
||||
const minX = xAxis?.min
|
||||
? xAxis.min instanceof Date
|
||||
? xAxis.min.getTime()
|
||||
@@ -976,8 +1030,8 @@ export class HaChartBase extends LitElement {
|
||||
? xAxis.max
|
||||
: undefined
|
||||
: undefined;
|
||||
return {
|
||||
...s,
|
||||
result = {
|
||||
...result,
|
||||
sampling: undefined,
|
||||
data: downSampleLineData(
|
||||
data as LineSeriesOption["data"],
|
||||
@@ -985,11 +1039,10 @@ export class HaChartBase extends LitElement {
|
||||
minX,
|
||||
maxX
|
||||
),
|
||||
};
|
||||
} as HaECSeriesItem;
|
||||
}
|
||||
}
|
||||
const name = filterXSS(String(s.name ?? s.id ?? ""));
|
||||
return { ...s, name, data };
|
||||
return processSeriesTooltipFormatter(result);
|
||||
});
|
||||
return series as ECOption["series"];
|
||||
}
|
||||
@@ -1326,8 +1379,8 @@ export class HaChartBase extends LitElement {
|
||||
}
|
||||
|
||||
private _compareCustomLegendOptions(
|
||||
oldOptions: ECOption | undefined,
|
||||
newOptions: ECOption | undefined
|
||||
oldOptions: HaECOption | undefined,
|
||||
newOptions: HaECOption | undefined
|
||||
): boolean {
|
||||
const oldLegends = ensureArray(
|
||||
oldOptions?.legend || []
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
|
||||
@customElement("ha-chart-tooltip-marker")
|
||||
class HaChartTooltipMarker extends LitElement {
|
||||
@property() public color = "";
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public rtl = false;
|
||||
|
||||
protected willUpdate(changed: PropertyValues) {
|
||||
if (changed.has("color")) {
|
||||
this.style.backgroundColor = this.color;
|
||||
}
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
:host {
|
||||
display: inline-block;
|
||||
margin-inline-end: 4px;
|
||||
margin-inline-start: initial;
|
||||
border-radius: 10px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
:host([rtl]) {
|
||||
direction: rtl;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-chart-tooltip-marker": HaChartTooltipMarker;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { EChartsType } from "echarts/core";
|
||||
import type { GraphSeriesOption } from "echarts/charts";
|
||||
import type { PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state, query } from "lit/decorators";
|
||||
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { listenMediaQuery } from "../../common/dom/media_query";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import "./ha-chart-base";
|
||||
import type { HaChartBase } from "./ha-chart-base";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
@@ -78,7 +78,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
|
||||
@property({ attribute: false }) public tooltipFormatter?: (
|
||||
params: TopLevelFormatterParams
|
||||
) => string;
|
||||
) => TemplateResult | typeof nothing | null;
|
||||
|
||||
/**
|
||||
* Optional callback that returns additional searchable strings for a node.
|
||||
@@ -182,7 +182,7 @@ export class HaNetworkGraph extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
(categories?: NetworkData["categories"]): ECOption => ({
|
||||
(categories?: NetworkData["categories"]): HaECOption => ({
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
confine: true,
|
||||
|
||||
@@ -11,10 +11,10 @@ import { ResizeController } from "@lit-labs/observers/resize-controller";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import SankeyChart from "../../resources/echarts/components/sankey/install";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import { measureTextWidth } from "../../util/text";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import "./ha-chart-base";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import { NODE_SIZE } from "../trace/hat-graph-const";
|
||||
import "../ha-alert";
|
||||
|
||||
@@ -71,7 +71,7 @@ export class HaSankeyChart extends LitElement {
|
||||
});
|
||||
|
||||
render() {
|
||||
const options = {
|
||||
const options: HaECOption = {
|
||||
grid: {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
@@ -83,7 +83,7 @@ export class HaSankeyChart extends LitElement {
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
} as ECOption;
|
||||
};
|
||||
|
||||
return html`<ha-chart-base
|
||||
.hass=${this.hass}
|
||||
@@ -103,12 +103,16 @@ export class HaSankeyChart extends LitElement {
|
||||
: data.value;
|
||||
if (data.id) {
|
||||
const node = this.data.nodes.find((n) => n.id === data.id);
|
||||
return `${params.marker} ${filterXSS(node?.label ?? data.id)}<br>${value}`;
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${node?.label ?? data.id}<br />${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 `${filterXSS(source?.label ?? data.source)} → ${filterXSS(target?.label ?? data.target)}<br>${value}`;
|
||||
return html`${source?.label ?? data.source} →
|
||||
${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 { filterXSS } from "../../common/util/xss";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-chart-base";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-imports
|
||||
let SunburstChart: typeof import("echarts/lib/chart/sunburst/install");
|
||||
@@ -50,13 +50,13 @@ export class HaSunburstChart extends LitElement {
|
||||
return nothing;
|
||||
}
|
||||
|
||||
const options = {
|
||||
const options: HaECOption = {
|
||||
tooltip: {
|
||||
trigger: "item",
|
||||
formatter: this._renderTooltip,
|
||||
appendTo: document.body,
|
||||
},
|
||||
} as ECOption;
|
||||
};
|
||||
|
||||
return html`<ha-chart-base
|
||||
.data=${this._createData(this.data)}
|
||||
@@ -71,7 +71,10 @@ export class HaSunburstChart extends LitElement {
|
||||
const value = this.valueFormatter
|
||||
? this.valueFormatter(data.value)
|
||||
: data.value;
|
||||
return `${params.marker} ${filterXSS(data.name)}<br>${value}`;
|
||||
return html`<ha-chart-tooltip-marker
|
||||
.color=${String(params.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${data.name}<br />${value}`;
|
||||
};
|
||||
|
||||
private _createData = memoizeOne(
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { nothing, render } from "lit";
|
||||
import type { LitTooltipFormatter } from "../../resources/echarts/echarts";
|
||||
|
||||
type WrappedTooltipFormatter = (
|
||||
params: unknown,
|
||||
ticket?: string
|
||||
) => HTMLElement | null;
|
||||
|
||||
export type { WrappedTooltipFormatter };
|
||||
|
||||
const litTooltipFormatterCache = new WeakMap<
|
||||
LitTooltipFormatter | WrappedTooltipFormatter,
|
||||
WrappedTooltipFormatter
|
||||
>();
|
||||
|
||||
export const wrapLitTooltipFormatter = (
|
||||
fn: LitTooltipFormatter | WrappedTooltipFormatter
|
||||
): WrappedTooltipFormatter => {
|
||||
const cached = litTooltipFormatterCache.get(fn);
|
||||
if (cached) return cached;
|
||||
const container = document.createElement("div");
|
||||
// display:contents keeps the wrapper layout-invisible so its children act as
|
||||
// direct children of echarts' tooltip box, matching the prior innerHTML behavior.
|
||||
container.style.display = "contents";
|
||||
const wrapped: WrappedTooltipFormatter = (params, ticket) => {
|
||||
const result = (fn as LitTooltipFormatter)(params, ticket);
|
||||
// `nothing` and null/undefined must all suppress the tooltip. Returning
|
||||
// `nothing` to echarts via `render(nothing, container)` leaves a Lit
|
||||
// comment marker behind so echarts would show an empty box; convert it to
|
||||
// null instead so `setContent(null)` clears innerHTML and `show()` hides.
|
||||
if (result === null || result === undefined || result === nothing) {
|
||||
return null;
|
||||
}
|
||||
render(result, container);
|
||||
return container;
|
||||
};
|
||||
litTooltipFormatterCache.set(fn, wrapped);
|
||||
// Idempotent re-wrap: looking up the wrapped fn returns itself.
|
||||
litTooltipFormatterCache.set(wrapped, wrapped);
|
||||
return wrapped;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { VisualMapComponentOption } from "echarts/components";
|
||||
import type { LineSeriesOption } from "echarts/charts";
|
||||
@@ -12,8 +12,9 @@ import type { LineChartEntity, LineChartState } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
import {
|
||||
getNumberFormatOptions,
|
||||
@@ -24,7 +25,6 @@ import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
||||
import { blankBeforeUnit } from "../../common/translations/blank_before_unit";
|
||||
import { filterXSS } from "../../common/util/xss";
|
||||
import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display";
|
||||
|
||||
const safeParseFloat = (value) => {
|
||||
@@ -108,7 +108,7 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _datasetToDataIndex: number[] = [];
|
||||
|
||||
@state() private _chartOptions?: ECOption;
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
|
||||
private _hiddenStats = new Set<string>();
|
||||
|
||||
@@ -141,12 +141,11 @@ export class StateHistoryChartLine extends LitElement {
|
||||
|
||||
private _renderTooltip = (params: any) => {
|
||||
const time = params[0].axisValue;
|
||||
const title =
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) + "<br>";
|
||||
const title = formatDateTimeWithSeconds(
|
||||
new Date(time),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
);
|
||||
const datapoints: Record<string, any>[] = [];
|
||||
this._chartData.forEach((dataset, index) => {
|
||||
if (
|
||||
@@ -177,52 +176,44 @@ export class StateHistoryChartLine extends LitElement {
|
||||
seriesName: dataset.name,
|
||||
seriesIndex: index,
|
||||
value: lastData,
|
||||
// HTML copied from echarts. May change based on options
|
||||
marker: `<span style="display:inline-block;margin-right:4px;margin-inline-end:4px;margin-inline-start:initial;border-radius:10px;width:10px;height:10px;background-color:${dataset.color};"></span>`,
|
||||
color: dataset.color,
|
||||
});
|
||||
});
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
|
||||
return (
|
||||
title +
|
||||
datapoints
|
||||
.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
let value = stateObj
|
||||
? this.hass.formatEntityState(stateObj, stateValue)
|
||||
: `${formatNumber(
|
||||
stateValue,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(undefined, entry)
|
||||
)}${unit}`;
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
value += "<br> ";
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? `${this.hass.localize(
|
||||
"ui.components.history_charts.source_stats"
|
||||
)}`
|
||||
: `${this.hass.localize(
|
||||
"ui.components.history_charts.source_history"
|
||||
)}`;
|
||||
value += source;
|
||||
}
|
||||
|
||||
if (param.seriesName) {
|
||||
return `${param.marker} ${filterXSS(param.seriesName)}: ${value}`;
|
||||
}
|
||||
return `${param.marker} ${value}`;
|
||||
})
|
||||
.join("<br>")
|
||||
);
|
||||
return html`${title}${datapoints.map((param) => {
|
||||
const entityId = this._entityIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[entityId];
|
||||
const entry = this.hass.entities[entityId];
|
||||
const stateValue = String(param.value[1]);
|
||||
const value = stateObj
|
||||
? this.hass.formatEntityState(stateObj, stateValue)
|
||||
: `${formatNumber(
|
||||
stateValue,
|
||||
this.hass.locale,
|
||||
getNumberFormatOptions(undefined, entry)
|
||||
)}${unit}`;
|
||||
const dataIndex = this._datasetToDataIndex[param.seriesIndex];
|
||||
const data = this.data[dataIndex];
|
||||
let statSuffix: TemplateResult | typeof nothing = nothing;
|
||||
if (data.statistics && data.statistics.length > 0) {
|
||||
const source =
|
||||
data.states.length === 0 ||
|
||||
param.value[0] < data.states[0].last_changed
|
||||
? this.hass.localize("ui.components.history_charts.source_stats")
|
||||
: this.hass.localize("ui.components.history_charts.source_history");
|
||||
// Five non-breaking spaces indent the source label.
|
||||
statSuffix = html`<br />${"\u00a0".repeat(5)}${source}`;
|
||||
}
|
||||
return html`<br /><ha-chart-tooltip-marker
|
||||
.color=${String(param.color ?? "")}
|
||||
></ha-chart-tooltip-marker>
|
||||
${param.seriesName
|
||||
? html`${param.seriesName}: `
|
||||
: nothing}${value}${statSuffix}`;
|
||||
})}`;
|
||||
};
|
||||
|
||||
private _datasetHidden(ev: CustomEvent) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type {
|
||||
CustomSeriesOption,
|
||||
CustomSeriesRenderItem,
|
||||
ECElementEvent,
|
||||
TooltipFormatterCallback,
|
||||
TooltipPositionCallbackParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||
@@ -15,8 +14,9 @@ import type { TimelineEntity } from "../../data/history";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import { computeTimelineColor } from "./timeline-color";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption, HaECSeries } from "../../resources/echarts/echarts";
|
||||
import echarts from "../../resources/echarts/echarts";
|
||||
import { luminosity } from "../../common/color/rgb";
|
||||
import { hex2rgb } from "../../common/color/convert-color";
|
||||
@@ -57,7 +57,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
|
||||
@state() private _chartData: CustomSeriesOption[] = [];
|
||||
|
||||
@state() private _chartOptions?: ECOption;
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
|
||||
@state() private _yWidth = 0;
|
||||
|
||||
@@ -69,7 +69,7 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.options=${this._chartOptions}
|
||||
.height=${`${this.data.length * 30 + 30}px`}
|
||||
.data=${this._chartData as ECOption["series"]}
|
||||
.data=${this._chartData as HaECSeries}
|
||||
small-controls
|
||||
@chart-click=${this._handleChartClick}
|
||||
@chart-zoom=${this._handleDataZoom}
|
||||
@@ -132,42 +132,35 @@ export class StateHistoryChartTimeline extends LitElement {
|
||||
return rect;
|
||||
};
|
||||
|
||||
private _renderTooltip: TooltipFormatterCallback<TooltipPositionCallbackParams> =
|
||||
(params: TooltipPositionCallbackParams) => {
|
||||
const { value, name, marker, seriesName, color } = Array.isArray(params)
|
||||
? params[0]
|
||||
: params;
|
||||
const title = seriesName
|
||||
? `<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: "";
|
||||
const durationInMs = value![2] - value![1];
|
||||
const formattedDuration = `${this.hass.localize(
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
private _renderTooltip = (params: TooltipPositionCallbackParams) => {
|
||||
const { value, name, seriesName, color } = Array.isArray(params)
|
||||
? params[0]
|
||||
: params;
|
||||
const durationInMs = value![2] - value![1];
|
||||
const formattedDuration = `${this.hass.localize(
|
||||
"ui.components.history_charts.duration"
|
||||
)}: ${millisecondsToDuration(durationInMs)}`;
|
||||
|
||||
const markerLocalized = !computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
)
|
||||
? marker
|
||||
: `<span style="direction: rtl;display:inline-block;margin-right:4px;margin-inline-end:4px;border-radius:10px;width:10px;height:10px;background-color:${color};"></span>`;
|
||||
|
||||
const lines = [
|
||||
markerLocalized + name,
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(value![1]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formatDateTimeWithSeconds(
|
||||
new Date(value![2]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
),
|
||||
formattedDuration,
|
||||
].join("<br>");
|
||||
return [title, lines].join("");
|
||||
};
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
);
|
||||
return html`${seriesName
|
||||
? html`<h4 style="text-align: center; margin: 0;">${seriesName}</h4>`
|
||||
: nothing}<ha-chart-tooltip-marker
|
||||
.color=${String(color ?? "")}
|
||||
.rtl=${rtl}
|
||||
></ha-chart-tooltip-marker
|
||||
>${name}<br />${formatDateTimeWithSeconds(
|
||||
new Date(value![1]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}<br />${formatDateTimeWithSeconds(
|
||||
new Date(value![2]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}<br />${formattedDuration}`;
|
||||
};
|
||||
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
if (
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
ZRColor,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { styleMap } from "lit/directives/style-map";
|
||||
import memoizeOne from "memoize-one";
|
||||
@@ -34,12 +34,13 @@ import {
|
||||
isExternalStatistic,
|
||||
statisticsHaveType,
|
||||
} from "../../data/recorder";
|
||||
import type { ECOption } from "../../resources/echarts/echarts";
|
||||
import type { HaECOption } from "../../resources/echarts/echarts";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { getPeriodicAxisLabelConfig } from "./axis-label";
|
||||
import type { CustomLegendOption } from "./ha-chart-base";
|
||||
import "./ha-chart-base";
|
||||
import { sideTooltipPosition } from "./chart-tooltip-position";
|
||||
import "./ha-chart-tooltip-marker";
|
||||
import { fillDataGapsAndRoundCaps } from "./round-caps";
|
||||
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
|
||||
|
||||
@@ -126,7 +127,7 @@ export class StatisticsChart extends LitElement {
|
||||
|
||||
@state() private _statisticIds: string[] = [];
|
||||
|
||||
@state() private _chartOptions?: ECOption;
|
||||
@state() private _chartOptions?: HaECOption;
|
||||
|
||||
@state() private _hiddenStats = new Set<string>();
|
||||
|
||||
@@ -251,91 +252,101 @@ export class StatisticsChart extends LitElement {
|
||||
const unit = this.unit
|
||||
? `${blankBeforeUnit(this.unit, this.hass.locale)}${this.unit}`
|
||||
: "";
|
||||
return params
|
||||
.map((param, index: number) => {
|
||||
if (rendered[param.seriesIndex]) return "";
|
||||
rendered[param.seriesIndex] = true;
|
||||
const rows: {
|
||||
time?: string;
|
||||
color: string;
|
||||
seriesName?: string;
|
||||
value: string;
|
||||
}[] = [];
|
||||
for (const param of params) {
|
||||
if (rendered[param.seriesIndex]) continue;
|
||||
rendered[param.seriesIndex] = true;
|
||||
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
let rawValue: string;
|
||||
let rawTime: string;
|
||||
if (chartIsBar) {
|
||||
// For bar charts value is always second value.
|
||||
rawValue = String(param.value[1]);
|
||||
// Time value is third value (un-shifted date) if given, otherwise first value
|
||||
let startTime: Date;
|
||||
let endTime: Date | undefined;
|
||||
if (param.value[2]) {
|
||||
startTime = new Date(param.value[2]);
|
||||
if (param.value[3]) {
|
||||
endTime = new Date(param.value[3]);
|
||||
}
|
||||
} else {
|
||||
startTime = new Date(param.value[0]);
|
||||
}
|
||||
if (
|
||||
period === "year" ||
|
||||
period === "month" ||
|
||||
period === "week" ||
|
||||
period === "day"
|
||||
) {
|
||||
// For year/month/day periods, show only the date
|
||||
rawTime =
|
||||
formatDate(startTime, this.hass.locale, this.hass.config) +
|
||||
(endTime && period !== "day"
|
||||
? ` – ${formatDate(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
} else {
|
||||
// For other time periods, include time in render, and optionally show range
|
||||
// if we have an end time.
|
||||
rawTime =
|
||||
formatDateTimeWithSeconds(
|
||||
startTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) +
|
||||
(endTime
|
||||
? ` – ${formatTimeWithSeconds(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "") +
|
||||
"<br>";
|
||||
const statisticId = this._statisticIds[param.seriesIndex];
|
||||
const stateObj = this.hass.states[statisticId];
|
||||
const entry = this.hass.entities[statisticId];
|
||||
let rawValue: string;
|
||||
let rawTime: string;
|
||||
if (chartIsBar) {
|
||||
// For bar charts value is always second value.
|
||||
rawValue = String(param.value[1]);
|
||||
// Time value is third value (un-shifted date) if given, otherwise first value
|
||||
let startTime: Date;
|
||||
let endTime: Date | undefined;
|
||||
if (param.value[2]) {
|
||||
startTime = new Date(param.value[2]);
|
||||
if (param.value[3]) {
|
||||
endTime = new Date(param.value[3]);
|
||||
}
|
||||
} else {
|
||||
// For lines max series can have 3 values, as the second value is the max-min to form a band
|
||||
rawValue = String(param.value[2] ?? param.value[1]);
|
||||
// Time value is always first value
|
||||
rawTime = `${formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)} <br>`;
|
||||
startTime = new Date(param.value[0]);
|
||||
}
|
||||
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(
|
||||
rawValue,
|
||||
if (
|
||||
period === "year" ||
|
||||
period === "month" ||
|
||||
period === "week" ||
|
||||
period === "day"
|
||||
) {
|
||||
// For year/month/day periods, show only the date
|
||||
rawTime =
|
||||
formatDate(startTime, this.hass.locale, this.hass.config) +
|
||||
(endTime && period !== "day"
|
||||
? ` – ${formatDate(endTime, this.hass.locale, this.hass.config)}`
|
||||
: "");
|
||||
} else {
|
||||
// For other time periods, include time in render, and optionally show range
|
||||
// if we have an end time.
|
||||
rawTime =
|
||||
formatDateTimeWithSeconds(
|
||||
startTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
) +
|
||||
(endTime
|
||||
? ` – ${formatTimeWithSeconds(
|
||||
endTime,
|
||||
this.hass.locale,
|
||||
this.hass.config
|
||||
)}`
|
||||
: "");
|
||||
}
|
||||
} else {
|
||||
// For lines max series can have 3 values, as the second value is the max-min to form a band
|
||||
rawValue = String(param.value[2] ?? param.value[1]);
|
||||
// Time value is always first value
|
||||
rawTime = formatDateTimeWithSeconds(
|
||||
new Date(param.value[0]),
|
||||
this.hass.locale,
|
||||
options
|
||||
)}${unit}`;
|
||||
this.hass.config
|
||||
);
|
||||
}
|
||||
|
||||
const time = index === 0 ? rawTime : "";
|
||||
return `${time}${param.marker} ${param.seriesName}: ${value}`;
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("<br>");
|
||||
const options = getNumberFormatOptions(stateObj, entry) ?? {
|
||||
maximumFractionDigits: 2,
|
||||
};
|
||||
|
||||
const value = `${formatNumber(rawValue, this.hass.locale, options)}${unit}`;
|
||||
|
||||
rows.push({
|
||||
time: rows.length === 0 ? rawTime : undefined,
|
||||
color: String(param.color ?? ""),
|
||||
seriesName: param.seriesName,
|
||||
value,
|
||||
});
|
||||
}
|
||||
|
||||
if (rows.length === 0) return nothing;
|
||||
|
||||
return html`${rows.map(
|
||||
(row, i) =>
|
||||
html`${row.time
|
||||
? html`${row.time}<br />`
|
||||
: nothing}<ha-chart-tooltip-marker
|
||||
.color=${row.color}
|
||||
></ha-chart-tooltip-marker>
|
||||
${row.seriesName}:
|
||||
${row.value}${i < rows.length - 1 ? html`<br />` : nothing}`
|
||||
)}`;
|
||||
};
|
||||
|
||||
private _createOptions() {
|
||||
|
||||
@@ -20,6 +20,7 @@ export class HaCheckListItem extends CheckListItemBase {
|
||||
separateCheckboxClick = false;
|
||||
|
||||
async onChange(event) {
|
||||
event.stopPropagation();
|
||||
super.onChange(event);
|
||||
fireEvent(this, event.type);
|
||||
}
|
||||
|
||||
@@ -99,8 +99,8 @@ export class HaRadioOption extends Radio {
|
||||
--ha-radio-option-checked-background-color,
|
||||
var(--ha-color-fill-primary-normal-resting)
|
||||
);
|
||||
color: var(--ha-color-fill-primary-loud-resting);
|
||||
border-color: var(--ha-color-fill-primary-loud-resting);
|
||||
color: var(--checked-icon-color);
|
||||
border-color: var(--checked-icon-color);
|
||||
}
|
||||
|
||||
[part~="label"] {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
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";
|
||||
@@ -15,6 +14,7 @@ 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,38 +97,36 @@ class PanelClimate extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<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}
|
||||
></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
|
||||
<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}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
></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
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -169,78 +167,11 @@ 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,17 +239,7 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
? html`<supervisor-apps-state
|
||||
.state=${this._currentAddon.state}
|
||||
></supervisor-apps-state>`
|
||||
: html`
|
||||
<ha-progress-button
|
||||
.disabled=${!this._currentAddon.available}
|
||||
@click=${this._installClicked}
|
||||
.iconPath=${mdiApplicationImport}
|
||||
>
|
||||
${this.i18n.localize(
|
||||
"ui.panel.config.apps.dashboard.install"
|
||||
)}
|
||||
</ha-progress-button>
|
||||
`}
|
||||
: nothing}
|
||||
</div>
|
||||
|
||||
<ha-chip-set class="capabilities">
|
||||
@@ -513,7 +503,8 @@ class SupervisorAppInfo extends MobileAwareMixin(LitElement) {
|
||||
</div>
|
||||
${(this._currentAddon.update_available && this._updateEntityId) ||
|
||||
this._computeShowWebUI ||
|
||||
this._computeShowIngressUI
|
||||
this._computeShowIngressUI ||
|
||||
!this._currentAddon.version
|
||||
? html`
|
||||
<div class="card-actions">
|
||||
${this._currentAddon.update_available && this._updateEntityId
|
||||
@@ -549,6 +540,19 @@ 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}
|
||||
@@ -1497,16 +1501,17 @@ 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 {
|
||||
@@ -1525,17 +1530,15 @@ 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: 60px;
|
||||
max-height: 40px;
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-assist-chip {
|
||||
--md-sys-color-primary: var(--text-primary-color);
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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";
|
||||
|
||||
@@ -16,13 +14,14 @@ 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`
|
||||
${this.state === "unknown"
|
||||
? html`<ha-svg-icon .path=${mdiHelpCircle}></ha-svg-icon>`
|
||||
: html` <div class="dot state-${this.state}"></div> `}
|
||||
<div class="dot state-${displayState}"></div>
|
||||
<span
|
||||
>${this._i18n.localize(
|
||||
`ui.panel.config.apps.dashboard.capability.state.${this.state}`
|
||||
`ui.panel.config.apps.dashboard.capability.state.${displayState}`
|
||||
)}</span
|
||||
>
|
||||
`;
|
||||
|
||||
@@ -334,13 +334,15 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
? this._renderTargets(
|
||||
target,
|
||||
actionHasTarget && !this._isNew,
|
||||
serviceTargetSpec
|
||||
serviceTargetSpec,
|
||||
type !== "device_id"
|
||||
)
|
||||
: nothing}
|
||||
${noteTooltipText
|
||||
? html`
|
||||
<ha-svg-icon
|
||||
id="note-icon"
|
||||
tabindex="0"
|
||||
.path=${mdiCommentTextOutline}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.note.label"
|
||||
@@ -721,13 +723,14 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"]
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
|
||||
@@ -30,10 +30,10 @@ import type {
|
||||
LocalizeFunc,
|
||||
LocalizeKeys,
|
||||
} from "../../../common/translations/localize";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { deepEqual } from "../../../common/util/deep-equal";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import "../../../components/entity/state-badge";
|
||||
import "../../../components/ha-bottom-sheet";
|
||||
import "../../../components/ha-button";
|
||||
@@ -134,8 +134,8 @@ import {
|
||||
ADD_AUTOMATION_ELEMENT_DEVICE_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_ENTITY_TARGET_PARAM,
|
||||
ADD_AUTOMATION_ELEMENT_QUERY_PARAM,
|
||||
getAddAutomationElementTargetFromQuery,
|
||||
PASTE_VALUE,
|
||||
getAddAutomationElementTargetFromQuery,
|
||||
} from "./show-add-automation-element-dialog";
|
||||
import { getTargetText } from "./target/get_target_text";
|
||||
|
||||
@@ -795,37 +795,33 @@ class DialogAddAutomationElement
|
||||
class="paste"
|
||||
@click=${this._paste}
|
||||
>
|
||||
<div class="shortcut-label">
|
||||
<div class="label">
|
||||
<div>
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.paste`
|
||||
)}
|
||||
</div>
|
||||
<div class="supporting-text">
|
||||
${this.hass.localize(
|
||||
// @ts-ignore
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
${!this._narrow
|
||||
? html`<span class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>V</span>
|
||||
</span>`
|
||||
: nothing}
|
||||
<div slot="headline" class="label">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.paste`
|
||||
)}
|
||||
</div>
|
||||
<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
// @ts-ignore
|
||||
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
|
||||
)}
|
||||
</div>
|
||||
${!this._narrow
|
||||
? html`<span slot="end" class="shortcut">
|
||||
<span
|
||||
>${isMac
|
||||
? html`<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiAppleKeyboardCommand}
|
||||
></ha-svg-icon>`
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.ctrl"
|
||||
)}</span
|
||||
>
|
||||
<span>+</span>
|
||||
<span>V</span>
|
||||
</span>`
|
||||
: nothing}
|
||||
<ha-svg-icon
|
||||
slot="start"
|
||||
.path=${mdiContentPaste}
|
||||
@@ -2546,23 +2542,16 @@ class DialogAddAutomationElement
|
||||
ha-svg-icon.plus {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
.shortcut-label {
|
||||
display: flex;
|
||||
gap: var(--ha-space-3);
|
||||
justify-content: space-between;
|
||||
}
|
||||
.shortcut-label .supporting-text {
|
||||
color: var(--secondary-text-color);
|
||||
font-size: var(--ha-font-size-s);
|
||||
}
|
||||
.shortcut-label .shortcut {
|
||||
|
||||
.shortcut {
|
||||
--mdc-icon-size: var(--ha-space-3);
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
margin-right: var(--ha-space-4);
|
||||
}
|
||||
.shortcut-label .shortcut span {
|
||||
.shortcut span {
|
||||
font-size: var(--ha-font-size-s);
|
||||
font-family: var(--ha-font-family-code);
|
||||
color: var(--ha-color-text-secondary);
|
||||
|
||||
@@ -224,13 +224,15 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
? this._renderTargets(
|
||||
target,
|
||||
descriptionHasTarget && !this._isNew,
|
||||
conditionTargetSpec
|
||||
conditionTargetSpec,
|
||||
this.condition.condition !== "device"
|
||||
)
|
||||
: 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"
|
||||
@@ -573,13 +575,14 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"]
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
|
||||
@@ -161,6 +161,7 @@ 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"
|
||||
|
||||
@@ -60,6 +60,9 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
@property({ attribute: false })
|
||||
public selector?: TargetSelector;
|
||||
|
||||
@property({ type: Boolean })
|
||||
public interactive = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: ContextType<typeof internationalizationContext>;
|
||||
@@ -89,7 +92,12 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
@consume({ context: statesContext, subscribe: true })
|
||||
private _states!: ContextType<typeof statesContext>;
|
||||
|
||||
private _countCache = new Map<string, Promise<number | undefined>>();
|
||||
private _countCache = new Map<
|
||||
string,
|
||||
Promise<number | undefined> | number | undefined
|
||||
>();
|
||||
|
||||
private _rerenderCount = true;
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
@@ -98,10 +106,15 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
changedProps.has("selector") ||
|
||||
changedProps.has("_registries")
|
||||
) {
|
||||
this._countCache.clear();
|
||||
this._rerenderCount = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues) {
|
||||
super.updated(changedProps);
|
||||
this._rerenderCount = false;
|
||||
}
|
||||
|
||||
private _countMatchingEntities(referencedEntities: string[]): number {
|
||||
const targetSelector = this.selector;
|
||||
const hasEntityFilter = !!targetSelector?.target?.entity;
|
||||
@@ -148,7 +161,11 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetId: string
|
||||
) {
|
||||
const key = `${targetType}:${targetId}`;
|
||||
if (!this._countCache.has(key)) {
|
||||
let fallback = " (-)";
|
||||
if (!this._countCache.has(key) || this._rerenderCount) {
|
||||
if (typeof this._countCache.get(key) === "number") {
|
||||
fallback = ` (${this._countCache.get(key)})`;
|
||||
}
|
||||
this._countCache.set(
|
||||
key,
|
||||
extractFromTarget(
|
||||
@@ -162,15 +179,30 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
.then((result) =>
|
||||
this._countMatchingEntities(result.referenced_entities)
|
||||
)
|
||||
.catch(() => undefined)
|
||||
.catch((err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Error counting target entities", err);
|
||||
return undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
return until(
|
||||
this._countCache
|
||||
.get(key)!
|
||||
.then((count) => (count === undefined ? nothing : html` (${count})`)),
|
||||
"(-)"
|
||||
);
|
||||
|
||||
if (this._countCache.get(key) instanceof Promise) {
|
||||
return until(
|
||||
(this._countCache.get(key) as Promise<number | undefined>)!.then(
|
||||
(count) => {
|
||||
this._countCache.set(key, count);
|
||||
return count === undefined ? nothing : html` (${count})`;
|
||||
}
|
||||
),
|
||||
fallback
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof this._countCache.get(key) === "number") {
|
||||
return ` (${this._countCache.get(key)})`;
|
||||
}
|
||||
return nothing;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
@@ -249,8 +281,9 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
<ha-dropdown
|
||||
@wa-select=${this._handleTargetSelect}
|
||||
@click=${stopPropagation}
|
||||
@keydown=${stopPropagation}
|
||||
>
|
||||
<span slot="trigger" class="target interactive">
|
||||
<button slot="trigger" class="target">
|
||||
<ha-svg-icon .path=${mdiFormatListBulleted}></ha-svg-icon>
|
||||
<div class="label">
|
||||
${this._i18n.localize(
|
||||
@@ -261,7 +294,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
)}
|
||||
</div>
|
||||
<ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon>
|
||||
</span>
|
||||
</button>
|
||||
${rows.map(([targetType, targetId]) => {
|
||||
const content = html`${lastTargetType !== null &&
|
||||
lastTargetType !== targetType
|
||||
@@ -316,21 +349,37 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetType?: string,
|
||||
countTemplate: unknown = nothing
|
||||
) {
|
||||
return html`<div
|
||||
if (!this.interactive || !targetId || !targetType) {
|
||||
return html`<div
|
||||
class=${classMap({
|
||||
target: true,
|
||||
warning,
|
||||
error,
|
||||
})}
|
||||
.targetId=${targetId}
|
||||
.targetType=${targetType}
|
||||
.label=${label}
|
||||
>
|
||||
${icon}
|
||||
<div class="label">${label}${countTemplate}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return html`<button
|
||||
class=${classMap({
|
||||
target: true,
|
||||
warning,
|
||||
error,
|
||||
interactive: targetId && targetType,
|
||||
})}
|
||||
.targetId=${targetId}
|
||||
.targetType=${targetType}
|
||||
.label=${label}
|
||||
@click=${this._handleTargetClick}
|
||||
@keydown=${this._handleTargetKeydown}
|
||||
>
|
||||
${icon}
|
||||
<div class="label">${label}${countTemplate}</div>
|
||||
</div>`;
|
||||
</button>`;
|
||||
}
|
||||
|
||||
private _renderTarget(
|
||||
@@ -384,7 +433,7 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
targetId,
|
||||
this._getLabel
|
||||
);
|
||||
if (targetType !== "entity") {
|
||||
if (targetType !== "entity" && this.interactive) {
|
||||
countTemplate = this._renderCount(targetType, targetId);
|
||||
}
|
||||
}
|
||||
@@ -444,6 +493,13 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
this._showTargetInfo(target.targetId, target.targetType, target.label, ev);
|
||||
}
|
||||
|
||||
private _handleTargetKeydown(ev: KeyboardEvent) {
|
||||
if (ev.key === "Enter" || ev.key === " ") {
|
||||
ev.preventDefault();
|
||||
this._handleTargetClick(ev);
|
||||
}
|
||||
}
|
||||
|
||||
private _handleTargetSelect(
|
||||
ev: HaDropdownSelectEvent<{
|
||||
targetId?: string;
|
||||
@@ -533,10 +589,10 @@ export class HaAutomationRowTargets extends LitElement {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.target.interactive {
|
||||
button.target {
|
||||
cursor: pointer;
|
||||
}
|
||||
.target.interactive:hover {
|
||||
button.target:hover {
|
||||
background: var(--ha-color-fill-neutral-normal-hover);
|
||||
}
|
||||
|
||||
|
||||
@@ -249,13 +249,15 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
? this._renderTargets(
|
||||
target,
|
||||
descriptionHasTarget && !this._isNew,
|
||||
triggerTargetSpec
|
||||
triggerTargetSpec,
|
||||
type !== "device"
|
||||
)
|
||||
: 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(
|
||||
@@ -557,13 +559,14 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
(
|
||||
target?: HassServiceTarget,
|
||||
targetRequired = false,
|
||||
targetSpec?: TargetSelector["target"]
|
||||
targetSpec?: TargetSelector["target"],
|
||||
interactive = false
|
||||
) =>
|
||||
html`<ha-automation-row-targets
|
||||
.hass=${this.hass}
|
||||
.target=${target}
|
||||
.targetRequired=${targetRequired}
|
||||
.selector=${targetSpec ? { target: targetSpec } : undefined}
|
||||
.interactive=${interactive}
|
||||
></ha-automation-row-targets>`
|
||||
);
|
||||
|
||||
|
||||
@@ -217,7 +217,7 @@ export class CloudRegister extends LitElement {
|
||||
|
||||
try {
|
||||
await cloudRegister(this.hass, email, password);
|
||||
this._verificationEmailSent(email);
|
||||
this._verificationEmailSent(email, "account_created");
|
||||
} catch (err: any) {
|
||||
this._password = "";
|
||||
this._requestInProgress = false;
|
||||
@@ -238,15 +238,18 @@ export class CloudRegister extends LitElement {
|
||||
|
||||
const email = emailField.value || "";
|
||||
|
||||
this._requestInProgress = true;
|
||||
|
||||
const doResend = async (username: string) => {
|
||||
try {
|
||||
await cloudResendVerification(this.hass, username);
|
||||
this._verificationEmailSent(username);
|
||||
this._verificationEmailSent(username, "verification_email_sent");
|
||||
} catch (err: any) {
|
||||
const errCode = err && err.body && err.body.code;
|
||||
if (errCode === "usernotfound" && username !== username.toLowerCase()) {
|
||||
await doResend(username.toLowerCase());
|
||||
} else {
|
||||
this._requestInProgress = false;
|
||||
this._error =
|
||||
err && err.body && err.body.message
|
||||
? err.body.message
|
||||
@@ -258,13 +261,16 @@ export class CloudRegister extends LitElement {
|
||||
await doResend(email);
|
||||
}
|
||||
|
||||
private _verificationEmailSent(email: string) {
|
||||
private _verificationEmailSent(
|
||||
email: string,
|
||||
messageKey: "account_created" | "verification_email_sent"
|
||||
) {
|
||||
this._requestInProgress = false;
|
||||
this._password = "";
|
||||
fireEvent(this, "cloud-email-changed", { value: email });
|
||||
fireEvent(this, "cloud-done", {
|
||||
flashMessage: this.hass.localize(
|
||||
"ui.panel.config.cloud.register.account_created"
|
||||
`ui.panel.config.cloud.register.${messageKey}`
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -330,42 +330,48 @@ export class BluetoothNetworkVisualization extends LitElement {
|
||||
return rssi > -33 ? 3 : rssi > -66 ? 2 : 1;
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams) => {
|
||||
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);
|
||||
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}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return html`${sourceName} →
|
||||
${targetName}${source !== CORE_SOURCE_ID
|
||||
? html` <b>${this.hass.localize("ui.panel.config.bluetooth.rssi")}:</b>
|
||||
${value}`
|
||||
: nothing}`;
|
||||
}
|
||||
return tooltipText;
|
||||
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;
|
||||
};
|
||||
|
||||
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): string => {
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams) => {
|
||||
const { dataType, data, name } = params as CallbackDataParams;
|
||||
if (dataType === "edge") {
|
||||
const { source, target, value } = data as any;
|
||||
@@ -141,40 +141,45 @@ 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;
|
||||
if (reverseValue) {
|
||||
return `${tooltipText}<br>${targetName} → ${sourceName} <b>LQI:</b> ${reverseValue}`;
|
||||
}
|
||||
return tooltipText;
|
||||
return html`${sourceName} →
|
||||
${targetName}${value
|
||||
? html` <b>LQI:</b> ${value}`
|
||||
: nothing}${reverseValue
|
||||
? html`<br />${targetName} → ${sourceName} <b>LQI:</b> ${reverseValue}`
|
||||
: nothing}`;
|
||||
}
|
||||
const device = this._devices.find((d) => d.ieee === (data as any).id);
|
||||
if (!device) {
|
||||
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>`;
|
||||
return html`${name}`;
|
||||
}
|
||||
const haDevice = this.hass.devices[device.device_reg_id] as
|
||||
| DeviceRegistryEntry
|
||||
| undefined;
|
||||
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;
|
||||
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}`;
|
||||
};
|
||||
|
||||
private async _refreshTopology(): Promise<void> {
|
||||
|
||||
@@ -9,6 +9,7 @@ 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,
|
||||
@@ -150,7 +151,7 @@ export class ZWaveJSNetworkVisualization extends SubscribeMixin(LitElement) {
|
||||
this._searchFilter = (ev.target as HaInputSearch).value ?? "";
|
||||
}
|
||||
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams): string => {
|
||||
private _tooltipFormatter = (params: TopLevelFormatterParams) => {
|
||||
const { dataType, data } = params as CallbackDataParams;
|
||||
if (dataType === "edge") {
|
||||
const { source, target, value } = data as any;
|
||||
@@ -160,39 +161,66 @@ 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;
|
||||
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;
|
||||
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}`;
|
||||
}
|
||||
const { id, name } = data as any;
|
||||
const device = this._devices[id] as DeviceRegistryEntry | undefined;
|
||||
const nodeStatus = this._nodeStatuses[id];
|
||||
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;
|
||||
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}`;
|
||||
};
|
||||
|
||||
private _getNetworkData = memoizeOne(
|
||||
|
||||
@@ -158,6 +158,7 @@ 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,7 +1,6 @@
|
||||
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";
|
||||
@@ -15,6 +14,7 @@ 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,38 +97,36 @@ class PanelLight extends LitElement {
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<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}
|
||||
></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
|
||||
<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}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
></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
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
: nothing}
|
||||
</ha-top-app-bar-fixed>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -169,78 +167,11 @@ 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,4 +1,6 @@
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, nothing } from "lit";
|
||||
import {
|
||||
subHours,
|
||||
differenceInDays,
|
||||
@@ -31,10 +33,10 @@ import {
|
||||
formatDateWeekdayVeryShortDate,
|
||||
} from "../../../../../common/datetime/format_date";
|
||||
import { formatTime } from "../../../../../common/datetime/format_time";
|
||||
import type { ECOption } from "../../../../../resources/echarts/echarts";
|
||||
import { filterXSS } from "../../../../../common/util/xss";
|
||||
import type { HaECOption } from "../../../../../resources/echarts/echarts";
|
||||
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";
|
||||
@@ -110,7 +112,7 @@ export function getCommonOptions(
|
||||
formatTotal?: (total: number) => string,
|
||||
detailedDailyData = false,
|
||||
yAxisFractionDigits = 1
|
||||
): ECOption {
|
||||
): HaECOption {
|
||||
const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData);
|
||||
let suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
|
||||
|
||||
@@ -134,7 +136,7 @@ export function getCommonOptions(
|
||||
}
|
||||
}
|
||||
|
||||
const monthTimeAxis: ECOption = {
|
||||
const monthTimeAxis: HaECOption = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: subDays(start, MONTH_TIME_AXIS_PADDING),
|
||||
@@ -146,7 +148,7 @@ export function getCommonOptions(
|
||||
splitNumber: Math.min(differenceInCalendarMonths(end, start), 5),
|
||||
},
|
||||
};
|
||||
const normalTimeAxis: ECOption = {
|
||||
const normalTimeAxis: HaECOption = {
|
||||
xAxis: {
|
||||
type: "time",
|
||||
min: start,
|
||||
@@ -154,7 +156,7 @@ export function getCommonOptions(
|
||||
},
|
||||
};
|
||||
|
||||
const options: ECOption = {
|
||||
const options: HaECOption = {
|
||||
...(suggestedPeriod === "month" ? monthTimeAxis : normalTimeAxis),
|
||||
yAxis: {
|
||||
type: "value",
|
||||
@@ -179,7 +181,7 @@ export function getCommonOptions(
|
||||
},
|
||||
tooltip: {
|
||||
trigger: "axis",
|
||||
formatter: (params: TopLevelFormatterParams): string => {
|
||||
formatter: (params: TopLevelFormatterParams) => {
|
||||
// trigger: "axis" gives an array of params, but "item" gives a single param
|
||||
if (Array.isArray(params)) {
|
||||
const mainItems: CallbackDataParams[] = [];
|
||||
@@ -191,7 +193,7 @@ export function getCommonOptions(
|
||||
mainItems.push(param);
|
||||
}
|
||||
});
|
||||
return [mainItems, compareItems]
|
||||
const sections = [mainItems, compareItems]
|
||||
.map((items) =>
|
||||
formatTooltip(
|
||||
items,
|
||||
@@ -204,8 +206,12 @@ export function getCommonOptions(
|
||||
formatTotal
|
||||
)
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join("<br><br>");
|
||||
.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}`
|
||||
)}`;
|
||||
}
|
||||
return formatTooltip(
|
||||
[params],
|
||||
@@ -232,9 +238,9 @@ function formatTooltip(
|
||||
showCompareYear: boolean,
|
||||
unit?: string,
|
||||
formatTotal?: (total: number) => string
|
||||
) {
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!params[0]?.value) {
|
||||
return "";
|
||||
return nothing;
|
||||
}
|
||||
// displayX may be shifted from the period start (see EnergyDataPoint);
|
||||
// originalStart has the real date for display. Gap-filled entries lack it.
|
||||
@@ -258,43 +264,50 @@ 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 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;
|
||||
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++;
|
||||
}
|
||||
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>`;
|
||||
}
|
||||
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 (sumNegative !== 0 && countNegative > 1 && formatTotal) {
|
||||
footer += `<br><b>${formatTotal(sumNegative)}</b>`;
|
||||
if (rows.length === 0) {
|
||||
return nothing;
|
||||
}
|
||||
return values.length > 0 ? `${title}${values.join("<br>")}${footer}` : "";
|
||||
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}`;
|
||||
}
|
||||
|
||||
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 { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { HaECOption } 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
|
||||
): ECOption => {
|
||||
): HaECOption => {
|
||||
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 { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import "../../../../components/ha-card";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { measureTextWidth } from "../../../../util/text";
|
||||
@@ -198,24 +198,28 @@ export class HuiEnergyDevicesGraphCard
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTooltip(params: any) {
|
||||
const deviceName = filterXSS(this._getDeviceName(params.name));
|
||||
const title = `<h4 style="text-align: center; margin: 0;">${deviceName}</h4>`;
|
||||
private _renderTooltip = (params: any) => {
|
||||
const deviceName = this._getDeviceName(params.name);
|
||||
const value = `${formatNumber(
|
||||
params.value[0] as number,
|
||||
this.hass.locale,
|
||||
params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined
|
||||
)} kWh ${params.percent ? `(${params.percent} %)` : ""}`;
|
||||
return `${title}${params.marker} ${params.seriesName}: <div style="direction:ltr; display: inline;">${value}</div>`;
|
||||
}
|
||||
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>`;
|
||||
};
|
||||
|
||||
private _createOptions = memoizeOne(
|
||||
(
|
||||
data: (BarSeriesOption | PieSeriesOption)[],
|
||||
chartType: "bar" | "pie",
|
||||
legendData: typeof this._legendData
|
||||
): ECOption => {
|
||||
const options: ECOption = {
|
||||
): HaECOption => {
|
||||
const options: HaECOption = {
|
||||
grid: {
|
||||
top: 5,
|
||||
left: 5,
|
||||
@@ -225,7 +229,7 @@ export class HuiEnergyDevicesGraphCard
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
formatter: this._renderTooltip.bind(this),
|
||||
formatter: this._renderTooltip,
|
||||
},
|
||||
xAxis: { show: false },
|
||||
yAxis: { show: false },
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
} from "./common/energy-chart-options";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { HaECOption } 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
|
||||
): ECOption =>
|
||||
): HaECOption =>
|
||||
getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
} from "./common/energy-chart-options";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
import "./common/hui-energy-graph-chip";
|
||||
import "../../../../components/ha-tooltip";
|
||||
|
||||
@@ -65,7 +65,7 @@ export class HuiEnergySolarGraphCard
|
||||
};
|
||||
}
|
||||
|
||||
@state() private _chartData: ECOption["series"][] = [];
|
||||
@state() private _chartData: (BarSeriesOption | LineSeriesOption)[] = [];
|
||||
|
||||
@state() private _yAxisFractionDigits = 1;
|
||||
|
||||
@@ -175,7 +175,7 @@ export class HuiEnergySolarGraphCard
|
||||
compareStart: Date | undefined,
|
||||
compareEnd: Date | undefined,
|
||||
yAxisFractionDigits: number
|
||||
): ECOption =>
|
||||
): HaECOption =>
|
||||
getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
@@ -213,7 +213,7 @@ export class HuiEnergySolarGraphCard
|
||||
}
|
||||
}
|
||||
|
||||
const datasets: ECOption["series"] = [];
|
||||
const datasets: (BarSeriesOption | LineSeriesOption)[] = [];
|
||||
|
||||
const computedStyles = getComputedStyle(this);
|
||||
|
||||
|
||||
@@ -6,10 +6,7 @@ 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 {
|
||||
TooltipOption,
|
||||
TopLevelFormatterParams,
|
||||
} from "echarts/types/dist/shared";
|
||||
import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
|
||||
import { getEnergyColor } from "./common/color";
|
||||
import { formatNumber } from "../../../../common/number/format_number";
|
||||
import "../../../../components/chart/ha-chart-base";
|
||||
@@ -43,7 +40,7 @@ import {
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
} from "./common/energy-chart-options";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { HaECOption } from "../../../../resources/echarts/echarts";
|
||||
|
||||
const colorPropertyMap = {
|
||||
to_grid: "--energy-grid-return-color",
|
||||
@@ -196,7 +193,7 @@ export class HuiEnergyUsageGraphCard
|
||||
compareStart: Date | undefined,
|
||||
compareEnd: Date | undefined,
|
||||
yAxisFractionDigits: number
|
||||
): ECOption => {
|
||||
): HaECOption => {
|
||||
const commonOptions = getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
@@ -209,15 +206,22 @@ export class HuiEnergyUsageGraphCard
|
||||
false,
|
||||
yAxisFractionDigits
|
||||
);
|
||||
const options: ECOption = {
|
||||
const tooltip = commonOptions.tooltip;
|
||||
const baseFormatter =
|
||||
tooltip &&
|
||||
!Array.isArray(tooltip) &&
|
||||
typeof tooltip.formatter === "function"
|
||||
? tooltip.formatter
|
||||
: undefined;
|
||||
const options: HaECOption = {
|
||||
...commonOptions,
|
||||
tooltip: {
|
||||
...commonOptions.tooltip,
|
||||
formatter: (params: TopLevelFormatterParams): string => {
|
||||
formatter: (params: TopLevelFormatterParams) => {
|
||||
if (!Array.isArray(params)) {
|
||||
return "";
|
||||
return nothing;
|
||||
}
|
||||
params.sort((a, b) => {
|
||||
const sorted = [...params].sort((a, b) => {
|
||||
const aValue = (a.value as number[])?.[1];
|
||||
const bValue = (b.value as number[])?.[1];
|
||||
if (aValue > 0 && bValue < 0) {
|
||||
@@ -231,9 +235,7 @@ export class HuiEnergyUsageGraphCard
|
||||
}
|
||||
return a.componentIndex - b.componentIndex;
|
||||
});
|
||||
return (
|
||||
(commonOptions.tooltip as TooltipOption)?.formatter as any
|
||||
)?.(params);
|
||||
return baseFormatter ? baseFormatter(sorted) : nothing;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
getCommonOptions,
|
||||
getCompareTransform,
|
||||
} from "./common/energy-chart-options";
|
||||
import type { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { HaECOption } 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
|
||||
): ECOption =>
|
||||
): HaECOption =>
|
||||
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 { ECOption } from "../../../../resources/echarts/echarts";
|
||||
import type { HaECOption } 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
|
||||
): ECOption => ({
|
||||
): HaECOption => ({
|
||||
...getCommonOptions(
|
||||
start,
|
||||
end,
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { consume } from "@lit/context";
|
||||
import { mdiAlertCircle, mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { css, html, LitElement } 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";
|
||||
@@ -28,17 +29,15 @@ const STATE_ICONS: Record<VisibilityState, string> = {
|
||||
|
||||
/**
|
||||
* @element ha-visibility-status
|
||||
* @extends {HaRowItem}
|
||||
*
|
||||
* @summary
|
||||
* 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.
|
||||
* Alert banner that surfaces the live visibility result for a set of
|
||||
* lovelace conditions.
|
||||
*
|
||||
* @attr {"visible"|"hidden"|"invalid"} state - Computed visibility state (reflected for styling).
|
||||
* @attr {"visible"|"hidden"|"invalid"} state - Computed visibility state
|
||||
*/
|
||||
@customElement("ha-visibility-status")
|
||||
export class HaVisibilityStatus extends HaRowItem {
|
||||
export class HaVisibilityStatus extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false })
|
||||
@@ -48,7 +47,7 @@ export class HaVisibilityStatus extends HaRowItem {
|
||||
@consume({ context: conditionsEntityContext, subscribe: true })
|
||||
private _entityContext?: ConditionsEntityContext;
|
||||
|
||||
@property({ reflect: true })
|
||||
@property()
|
||||
public state: VisibilityState = "visible";
|
||||
|
||||
private _listeners = new ConditionListenersController(this);
|
||||
@@ -71,23 +70,27 @@ export class HaVisibilityStatus extends HaRowItem {
|
||||
}
|
||||
}
|
||||
|
||||
protected override _renderInner(): TemplateResult {
|
||||
public render() {
|
||||
return html`
|
||||
<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">
|
||||
<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">
|
||||
${this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.headline`
|
||||
)}
|
||||
</div>
|
||||
<div part="supporting-text" class="supporting">
|
||||
<div class="supporting">
|
||||
${this.hass?.localize(
|
||||
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.supporting${(this.conditions?.length ?? 0) === 0 ? "_empty" : ""}`
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ha-alert>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -117,37 +120,13 @@ export class HaVisibilityStatus extends HaRowItem {
|
||||
static styles: CSSResultGroup = [
|
||||
HaRowItem.styles,
|
||||
css`
|
||||
:host {
|
||||
ha-alert {
|
||||
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);
|
||||
white-space: normal;
|
||||
margin-bottom: var(--ha-space-1);
|
||||
}
|
||||
`,
|
||||
];
|
||||
|
||||
@@ -67,6 +67,14 @@ export type ECOption = ComposeOption<
|
||||
| SunburstSeriesOption
|
||||
>;
|
||||
|
||||
export type {
|
||||
HaECOption,
|
||||
HaECSeries,
|
||||
HaECSeriesItem,
|
||||
HaTooltipOption,
|
||||
LitTooltipFormatter,
|
||||
} from "./ha-ec-option";
|
||||
|
||||
// Register the required components
|
||||
echarts.use([
|
||||
BarChart,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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];
|
||||
};
|
||||
@@ -6319,7 +6319,8 @@
|
||||
"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."
|
||||
"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": {
|
||||
"download_support_package": "Download support package",
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||