unique_bubble_graph.yaml aktualisiert

This commit is contained in:
2026-06-11 10:21:34 +00:00
parent f352d20589
commit c78fa144a4

View File

@@ -1,10 +1,10 @@
unique_bubble_graph: unique_bubble_graph:
name: Unique Bubble Multi History Background Graph name: Unique Bubble Multi History Background Graph
version: 2.6.0 version: 2.8.6
creator: Torsten creator: Torsten
supported: supported:
- button - button
description: Shows up to three Home Assistant sensor history graphs as Bubble Card background with data-point cache, persistent cache, centered loading spinner and extend-to-now. description: Shows up to three explicitly configured Home Assistant sensor history graphs as Bubble Card background with data-point cache, persistent cache, centered loading spinner, extend-to-now, peak-preserving downsampling and optional same-scale mode, fixed scale limits, percent-of-max value conversion and nested expandable editor settings support and RGB color picker array support and isolated per-graph color settings and cache-safe live style refresh.
code: | code: |
ha-card { ha-card {
position: relative !important; position: relative !important;
@@ -134,11 +134,34 @@ unique_bubble_graph:
} }
${(() => { ${(() => {
const cfg = const rawCfg =
this.config.unique_bubble_graph || this.config.unique_bubble_graph ||
this.config.history_background_graph || this.config.history_background_graph ||
{}; {};
const graphSettings = [
rawCfg.graph_1_settings || {},
rawCfg.graph_2_settings || {},
rawCfg.graph_3_settings || {}
];
const cfg = {
...(rawCfg.general_settings || {}),
...(rawCfg.scale_settings || {}),
...(rawCfg.advanced_settings || {}),
...rawCfg
};
const getGraphSetting = (index, key, fallback = undefined) => {
const prefix = `graph_${index + 1}`;
const group = graphSettings[index] || {};
return group[`${prefix}_${key}`] ??
group[key] ??
cfg[`${prefix}_${key}`] ??
fallback;
};
const hoursToShow = Number(cfg.hours_to_show ?? 24); const hoursToShow = Number(cfg.hours_to_show ?? 24);
const refreshMinutes = Number(cfg.refresh_minutes ?? 10); const refreshMinutes = Number(cfg.refresh_minutes ?? 10);
const maxPoints = Number(cfg.max_points ?? 120); const maxPoints = Number(cfg.max_points ?? 120);
@@ -153,80 +176,172 @@ unique_bubble_graph:
const extendToNow = cfg.extend_to_now !== false; const extendToNow = cfg.extend_to_now !== false;
const extendThresholdMinutes = Number(cfg.extend_threshold_minutes ?? 2); const extendThresholdMinutes = Number(cfg.extend_threshold_minutes ?? 2);
const sameScale = cfg.same_scale === true;
const numberOrNull = (value) => {
if (value === undefined || value === null || value === "") return null;
const number = Number(value);
return Number.isFinite(number) ? number : null;
};
const sameScaleMin = numberOrNull(cfg.same_scale_min);
const sameScaleMax = numberOrNull(cfg.same_scale_max);
const defaultLineWidth = Number(cfg.line_width ?? 2.5); const defaultLineWidth = Number(cfg.line_width ?? 2.5);
const defaultLineOpacity = Number(cfg.line_opacity ?? 0.75); const defaultLineOpacity = Number(cfg.line_opacity ?? 0.75);
const defaultFillOpacity = Number(cfg.fill_opacity ?? 0.08); const defaultFillOpacity = Number(cfg.fill_opacity ?? 0.08);
const normalizeColor = (color, fallback = "var(--primary-color)") => {
if (Array.isArray(color) && color.length >= 3) {
const clamp = (value) => Math.max(0, Math.min(255, Math.round(Number(value) || 0)));
const r = clamp(color[0]);
const g = clamp(color[1]);
const b = clamp(color[2]);
const a = color.length >= 4 ? Number(color[3]) : null;
if (Number.isFinite(a)) {
return `rgba(${r}, ${g}, ${b}, ${Math.max(0, Math.min(1, a))})`;
}
return `rgb(${r}, ${g}, ${b})`;
}
if (typeof color !== "string") return fallback;
const trimmed = color.trim();
if (!trimmed) return fallback;
if (
trimmed.startsWith("#") ||
trimmed.startsWith("rgb(") ||
trimmed.startsWith("rgba(") ||
trimmed.startsWith("hsl(") ||
trimmed.startsWith("hsla(") ||
trimmed.startsWith("var(")
) {
return trimmed;
}
if (/^[0-9a-fA-F]{3}$/.test(trimmed) || /^[0-9a-fA-F]{6}$/.test(trimmed)) {
return `#${trimmed}`;
}
return trimmed;
};
const defaultColors = [ const defaultColors = [
cfg.graph_1_color || cfg.line_color || "var(--primary-color)", "var(--primary-color)",
cfg.graph_2_color || "#2196f3", "#2196f3",
cfg.graph_3_color || "#4caf50" "#4caf50"
]; ];
const normalizeValueMode = (value) => {
return value === "percent_of_max" ? "percent_of_max" : "default";
};
const getGraphScaleConfig = (graph, index) => {
const valueMode = normalizeValueMode(
graph?.value_mode ??
graph?.valueMode ??
getGraphSetting(index, "value_mode")
);
const maxValue = numberOrNull(
graph?.max_value ??
graph?.maxValue ??
getGraphSetting(index, "max_value")
);
const effectiveValueMode =
valueMode === "percent_of_max" &&
Number.isFinite(maxValue) &&
maxValue > 0
? "percent_of_max"
: "default";
return {
valueMode: effectiveValueMode,
maxValue
};
};
const buildGraphs = () => { const buildGraphs = () => {
if (Array.isArray(cfg.graphs) && cfg.graphs.length) { if (Array.isArray(cfg.graphs) && cfg.graphs.length) {
return cfg.graphs return cfg.graphs
.slice(0, 3) .slice(0, 3)
.map((graph, index) => ({ .map((graph, index) => {
const scaleConfig = getGraphScaleConfig(graph, index);
return {
entity: graph.entity, entity: graph.entity,
label: label:
graph.label || graph.label ||
hass.states?.[graph.entity]?.attributes?.friendly_name || hass.states?.[graph.entity]?.attributes?.friendly_name ||
graph.entity || graph.entity ||
`Graph ${index + 1}`, `Graph ${index + 1}`,
color: graph.color || graph.line_color || defaultColors[index], color: normalizeColor(graph.color || graph.line_color, defaultColors[index]),
lineWidth: Number(graph.line_width ?? defaultLineWidth), lineWidth: Number(graph.line_width ?? defaultLineWidth),
lineOpacity: Number(graph.line_opacity ?? defaultLineOpacity), lineOpacity: Number(graph.line_opacity ?? defaultLineOpacity),
fillOpacity: Number(graph.fill_opacity ?? defaultFillOpacity), fillOpacity: Number(graph.fill_opacity ?? defaultFillOpacity),
unit: hass.states?.[graph.entity]?.attributes?.unit_of_measurement || "" rawUnit: hass.states?.[graph.entity]?.attributes?.unit_of_measurement || "",
})) unit: scaleConfig.valueMode === "percent_of_max" ? "%" : (hass.states?.[graph.entity]?.attributes?.unit_of_measurement || ""),
valueMode: scaleConfig.valueMode,
maxValue: scaleConfig.maxValue
};
})
.filter((graph) => graph.entity); .filter((graph) => graph.entity);
} }
const graph1Entity = cfg.graph_1_entity || cfg.entity || this.config.entity || "sensor.sma_tripower_x_pv_power"; const graph1Entity = getGraphSetting(0, "entity");
const graph2Entity = cfg.graph_2_entity; const graph2Entity = getGraphSetting(1, "entity");
const graph3Entity = cfg.graph_3_entity; const graph3Entity = getGraphSetting(2, "entity");
return [ return [
{ {
entity: graph1Entity, entity: graph1Entity,
label: label:
cfg.graph_1_label || getGraphSetting(0, "label") ||
cfg.label || cfg.label ||
hass.states?.[graph1Entity]?.attributes?.friendly_name || hass.states?.[graph1Entity]?.attributes?.friendly_name ||
graph1Entity || graph1Entity ||
"Graph 1", "Graph 1",
color: cfg.graph_1_color || cfg.line_color || defaultColors[0], color: normalizeColor(getGraphSetting(0, "color") || cfg.line_color, defaultColors[0]),
lineWidth: Number(cfg.graph_1_line_width ?? defaultLineWidth), lineWidth: Number(getGraphSetting(0, "line_width", defaultLineWidth)),
lineOpacity: Number(cfg.graph_1_line_opacity ?? defaultLineOpacity), lineOpacity: Number(getGraphSetting(0, "line_opacity", defaultLineOpacity)),
fillOpacity: Number(cfg.graph_1_fill_opacity ?? defaultFillOpacity), fillOpacity: Number(getGraphSetting(0, "fill_opacity", defaultFillOpacity)),
unit: hass.states?.[graph1Entity]?.attributes?.unit_of_measurement || "" rawUnit: hass.states?.[graph1Entity]?.attributes?.unit_of_measurement || "",
unit: getGraphScaleConfig(null, 0).valueMode === "percent_of_max" ? "%" : (hass.states?.[graph1Entity]?.attributes?.unit_of_measurement || ""),
valueMode: getGraphScaleConfig(null, 0).valueMode,
maxValue: getGraphScaleConfig(null, 0).maxValue
}, },
{ {
entity: graph2Entity, entity: graph2Entity,
label: label:
cfg.graph_2_label || getGraphSetting(1, "label") ||
hass.states?.[graph2Entity]?.attributes?.friendly_name || hass.states?.[graph2Entity]?.attributes?.friendly_name ||
graph2Entity || graph2Entity ||
"Graph 2", "Graph 2",
color: cfg.graph_2_color || defaultColors[1], color: normalizeColor(getGraphSetting(1, "color"), defaultColors[1]),
lineWidth: Number(cfg.graph_2_line_width ?? defaultLineWidth), lineWidth: Number(getGraphSetting(1, "line_width", defaultLineWidth)),
lineOpacity: Number(cfg.graph_2_line_opacity ?? defaultLineOpacity), lineOpacity: Number(getGraphSetting(1, "line_opacity", defaultLineOpacity)),
fillOpacity: Number(cfg.graph_2_fill_opacity ?? defaultFillOpacity), fillOpacity: Number(getGraphSetting(1, "fill_opacity", defaultFillOpacity)),
unit: hass.states?.[graph2Entity]?.attributes?.unit_of_measurement || "" rawUnit: hass.states?.[graph2Entity]?.attributes?.unit_of_measurement || "",
unit: getGraphScaleConfig(null, 1).valueMode === "percent_of_max" ? "%" : (hass.states?.[graph2Entity]?.attributes?.unit_of_measurement || ""),
valueMode: getGraphScaleConfig(null, 1).valueMode,
maxValue: getGraphScaleConfig(null, 1).maxValue
}, },
{ {
entity: graph3Entity, entity: graph3Entity,
label: label:
cfg.graph_3_label || getGraphSetting(2, "label") ||
hass.states?.[graph3Entity]?.attributes?.friendly_name || hass.states?.[graph3Entity]?.attributes?.friendly_name ||
graph3Entity || graph3Entity ||
"Graph 3", "Graph 3",
color: cfg.graph_3_color || defaultColors[2], color: normalizeColor(getGraphSetting(2, "color"), defaultColors[2]),
lineWidth: Number(cfg.graph_3_line_width ?? defaultLineWidth), lineWidth: Number(getGraphSetting(2, "line_width", defaultLineWidth)),
lineOpacity: Number(cfg.graph_3_line_opacity ?? defaultLineOpacity), lineOpacity: Number(getGraphSetting(2, "line_opacity", defaultLineOpacity)),
fillOpacity: Number(cfg.graph_3_fill_opacity ?? defaultFillOpacity), fillOpacity: Number(getGraphSetting(2, "fill_opacity", defaultFillOpacity)),
unit: hass.states?.[graph3Entity]?.attributes?.unit_of_measurement || "" rawUnit: hass.states?.[graph3Entity]?.attributes?.unit_of_measurement || "",
unit: getGraphScaleConfig(null, 2).valueMode === "percent_of_max" ? "%" : (hass.states?.[graph3Entity]?.attributes?.unit_of_measurement || ""),
valueMode: getGraphScaleConfig(null, 2).valueMode,
maxValue: getGraphScaleConfig(null, 2).maxValue
} }
].filter((graph) => graph.entity); ].filter((graph) => graph.entity);
}; };
@@ -299,6 +414,31 @@ unique_bubble_graph:
return `${value.toFixed(1)} ${unit}`.trim(); return `${value.toFixed(1)} ${unit}`.trim();
}; };
const formatTooltipValue = (point, series) => {
const value = formatValue(point.v, series.unit);
if (
series.valueMode === "percent_of_max" &&
Number.isFinite(point.rawV)
) {
return `${value} · ${formatValue(point.rawV, series.rawUnit)}`;
}
return value;
};
const transformValueForGraph = (value, graph) => {
if (
graph.valueMode === "percent_of_max" &&
Number.isFinite(graph.maxValue) &&
graph.maxValue > 0
) {
return (value / graph.maxValue) * 100;
}
return value;
};
const formatTime = (timestamp) => { const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleString("de-DE", { return new Date(timestamp).toLocaleString("de-DE", {
day: "2-digit", day: "2-digit",
@@ -325,6 +465,42 @@ unique_bubble_graph:
return nearest; return nearest;
}; };
const downsampleMinMax = (points, maxPoints) => {
if (!Array.isArray(points) || points.length <= maxPoints) return points;
const firstPoint = points[0];
const lastPoint = points[points.length - 1];
const bucketCount = Math.max(1, Math.floor((maxPoints - 2) / 2));
const bucketSize = Math.ceil(points.length / bucketCount);
const sampled = [firstPoint];
for (let index = 0; index < points.length; index += bucketSize) {
const bucket = points.slice(index, index + bucketSize);
if (!bucket.length) continue;
const min = bucket.reduce((a, b) => b.v < a.v ? b : a);
const max = bucket.reduce((a, b) => b.v > a.v ? b : a);
if (min.t <= max.t) {
sampled.push(min, max);
} else {
sampled.push(max, min);
}
}
sampled.push(lastPoint);
return sampled
.filter((point, index, array) =>
point &&
Number.isFinite(point.t) &&
Number.isFinite(point.v) &&
array.findIndex((item) => item.t === point.t && item.v === point.v) === index
)
.sort((a, b) => a.t - b.t)
.slice(0, maxPoints);
};
const extendSeriesToNow = (seriesList) => { const extendSeriesToNow = (seriesList) => {
if (!extendToNow) return seriesList; if (!extendToNow) return seriesList;
@@ -334,15 +510,18 @@ unique_bubble_graph:
return seriesList.map((series) => { return seriesList.map((series) => {
const points = [...(series.points || [])]; const points = [...(series.points || [])];
const lastPoint = points[points.length - 1]; const lastPoint = points[points.length - 1];
const currentValue = Number(hass.states?.[series.entity]?.state); const currentRawValue = Number(hass.states?.[series.entity]?.state);
const currentValue = transformValueForGraph(currentRawValue, series);
if ( if (
Number.isFinite(currentRawValue) &&
Number.isFinite(currentValue) && Number.isFinite(currentValue) &&
(!lastPoint || now - lastPoint.t > thresholdMs) (!lastPoint || now - lastPoint.t > thresholdMs)
) { ) {
points.push({ points.push({
t: now, t: now,
v: currentValue v: currentValue,
rawV: currentRawValue
}); });
} }
@@ -353,18 +532,41 @@ unique_bubble_graph:
}); });
}; };
const attachTooltip = (seriesList) => { const attachTooltipOnce = () => {
if (cfg.tooltip === false) return; if (cfg.tooltip === false) return;
host.__bubbleHistoryTooltip = tooltip;
host.__bubbleHistoryMarker = marker;
host.__bubbleHistoryTooltipTop = tooltipTop;
if (host.__bubbleHistoryTooltipAttached) return;
host.__bubbleHistoryTooltipAttached = true;
host.onmousemove = (event) => { host.onmousemove = (event) => {
const usableSeries = seriesList.filter((series) => series.points?.length); const usableSeries = (host.__bubbleHistorySeries || [])
.filter((series) => series.points?.length);
if (!usableSeries.length) return; if (!usableSeries.length) return;
const tooltipElement = host.__bubbleHistoryTooltip;
const markerElement = host.__bubbleHistoryMarker;
if (!tooltipElement || !markerElement) return;
const rect = host.getBoundingClientRect(); const rect = host.getBoundingClientRect();
const xRatio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width)); const xRatio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
const globalMinTime = Math.min(...usableSeries.map((series) => series.points[0].t)); const globalMinTime =
const globalMaxTime = Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t)); Number.isFinite(host.__bubbleHistoryMinTime)
? host.__bubbleHistoryMinTime
: Math.min(...usableSeries.map((series) => series.points[0].t));
const globalMaxTime =
Number.isFinite(host.__bubbleHistoryMaxTime)
? host.__bubbleHistoryMaxTime
: Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t));
const targetTime = globalMinTime + xRatio * (globalMaxTime - globalMinTime); const targetTime = globalMinTime + xRatio * (globalMaxTime - globalMinTime);
const rows = usableSeries.map((series) => { const rows = usableSeries.map((series) => {
@@ -374,7 +576,7 @@ unique_bubble_graph:
return ` return `
<div class="bubble-history-tooltip-row"> <div class="bubble-history-tooltip-row">
<span class="bubble-history-tooltip-dot" style="background:${series.color};"></span> <span class="bubble-history-tooltip-dot" style="background:${series.color};"></span>
<span><strong>${series.label}:</strong> ${formatValue(nearest.v, series.unit)}</span> <span><strong>${series.label}:</strong> ${formatTooltipValue(nearest, series)}</span>
</div> </div>
`; `;
}).join(""); }).join("");
@@ -385,22 +587,48 @@ unique_bubble_graph:
const x = ((targetTime - globalMinTime) / (globalMaxTime - globalMinTime || 1)) * rect.width; const x = ((targetTime - globalMinTime) / (globalMaxTime - globalMinTime || 1)) * rect.width;
const safeX = Math.min(rect.width - 60, Math.max(60, x)); const safeX = Math.min(rect.width - 60, Math.max(60, x));
tooltip.innerHTML = ` tooltipElement.innerHTML = `
${rows} ${rows}
<div style="opacity:0.75;margin-top:3px;">${formatTime(tooltipTime)}</div> <div style="opacity:0.75;margin-top:3px;">${formatTime(tooltipTime)}</div>
`; `;
tooltip.style.display = "block"; tooltipElement.style.display = "block";
marker.style.display = "block"; markerElement.style.display = "block";
tooltip.style.left = `${safeX}px`; tooltipElement.style.left = `${safeX}px`;
tooltip.style.top = `${tooltipTop}px`; tooltipElement.style.top = `${host.__bubbleHistoryTooltipTop}px`;
marker.style.left = `${x}px`; markerElement.style.left = `${x}px`;
}; };
host.onmouseleave = () => { host.onmouseleave = () => {
tooltip.style.display = "none"; const tooltipElement = host.__bubbleHistoryTooltip;
marker.style.display = "none"; const markerElement = host.__bubbleHistoryMarker;
if (tooltipElement) tooltipElement.style.display = "none";
if (markerElement) markerElement.style.display = "none";
};
};
const getSameScaleRange = (seriesList) => {
const allValues = seriesList.flatMap((series) =>
(series.points || []).map((point) => point.v)
);
if (!allValues.length) return null;
const rawMinValue = Math.min(...allValues);
const rawMaxValue = Math.max(...allValues);
const rawRange = rawMaxValue - rawMinValue;
const safeRange = rawRange || Math.max(Math.abs(rawMaxValue), 1);
const valuePadding = safeRange * (valuePaddingPercent / 100);
const calculatedMinValue = rawMinValue - valuePadding;
const calculatedMaxValue = rawMaxValue + valuePadding;
return {
minValue: Number.isFinite(sameScaleMin) ? sameScaleMin : calculatedMinValue,
maxValue: Number.isFinite(sameScaleMax) ? sameScaleMax : calculatedMaxValue
}; };
}; };
@@ -415,8 +643,16 @@ unique_bubble_graph:
const globalMinTime = Math.min(...usableSeries.map((series) => series.points[0].t)); const globalMinTime = Math.min(...usableSeries.map((series) => series.points[0].t));
const globalMaxTime = Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t)); const globalMaxTime = Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t));
const sameScaleRange = sameScale ? getSameScaleRange(usableSeries) : null;
const buildPath = (series) => { const buildPath = (series) => {
let minValue;
let maxValue;
if (sameScaleRange) {
minValue = sameScaleRange.minValue;
maxValue = sameScaleRange.maxValue;
} else {
const rawMinValue = Math.min(...series.points.map((point) => point.v)); const rawMinValue = Math.min(...series.points.map((point) => point.v));
const rawMaxValue = Math.max(...series.points.map((point) => point.v)); const rawMaxValue = Math.max(...series.points.map((point) => point.v));
@@ -424,8 +660,9 @@ unique_bubble_graph:
const safeRange = rawRange || Math.max(Math.abs(rawMaxValue), 1); const safeRange = rawRange || Math.max(Math.abs(rawMaxValue), 1);
const valuePadding = safeRange * (valuePaddingPercent / 100); const valuePadding = safeRange * (valuePaddingPercent / 100);
const minValue = rawMinValue - valuePadding; minValue = rawMinValue - valuePadding;
const maxValue = rawMaxValue + valuePadding; maxValue = rawMaxValue + valuePadding;
}
const timeRange = globalMaxTime - globalMinTime || 1; const timeRange = globalMaxTime - globalMinTime || 1;
const valueRange = maxValue - minValue || 1; const valueRange = maxValue - minValue || 1;
@@ -477,7 +714,10 @@ unique_bubble_graph:
</svg> </svg>
`; `;
attachTooltip(usableSeries); host.__bubbleHistorySeries = usableSeries;
host.__bubbleHistoryMinTime = globalMinTime;
host.__bubbleHistoryMaxTime = globalMaxTime;
attachTooltipOnce();
return true; return true;
}; };
@@ -500,13 +740,41 @@ unique_bubble_graph:
return ""; return "";
} }
const dataCacheParts = validGraphs.map((graph) => graph.entity).join("|"); const applyCurrentGraphSettingsToSeries = (seriesList) => {
if (!Array.isArray(seriesList)) return [];
return seriesList.map((cachedSeries) => {
const currentGraph = validGraphs.find((graph) => graph.entity === cachedSeries.entity);
if (!currentGraph) return cachedSeries;
return {
...cachedSeries,
label: currentGraph.label,
color: currentGraph.color,
lineWidth: currentGraph.lineWidth,
lineOpacity: currentGraph.lineOpacity,
fillOpacity: currentGraph.fillOpacity,
rawUnit: currentGraph.rawUnit,
unit: currentGraph.unit,
valueMode: currentGraph.valueMode,
maxValue: currentGraph.maxValue
};
});
};
const dataCacheParts = validGraphs
.map((graph) => `${graph.entity}:${graph.valueMode}:${graph.maxValue ?? ""}`)
.join("|");
const cacheKey = [ const cacheKey = [
"unique-bubble-graph-data-v260", "unique-bubble-graph-data-v285",
dataCacheParts, dataCacheParts,
hoursToShow, hoursToShow,
maxPoints maxPoints,
sameScale ? "same-scale" : "individual-scale",
sameScaleMin ?? "auto-min",
sameScaleMax ?? "auto-max"
].join("::"); ].join("::");
const storageKey = `unique-bubble-graph-data-cache:${cacheKey}`; const storageKey = `unique-bubble-graph-data-cache:${cacheKey}`;
@@ -558,6 +826,11 @@ unique_bubble_graph:
readPersistentCache(); readPersistentCache();
if (cache?.seriesList) { if (cache?.seriesList) {
cache = {
...cache,
seriesList: applyCurrentGraphSettingsToSeries(cache.seriesList)
};
renderGraph(cache.seriesList); renderGraph(cache.seriesList);
window.__uniqueBubbleGraphDataCache[cacheKey] = { window.__uniqueBubbleGraphDataCache[cacheKey] = {
@@ -600,26 +873,23 @@ unique_bubble_graph:
const states = Array.isArray(history) ? history.flat() : []; const states = Array.isArray(history) ? history.flat() : [];
let points = states let points = states
.map((state) => ({ .map((state) => {
const rawValue = Number(state.s ?? state.state);
return {
t: new Date(state.lu || state.lc || state.last_updated || state.last_changed).getTime(), t: new Date(state.lu || state.lc || state.last_updated || state.last_changed).getTime(),
v: Number(state.s ?? state.state) v: transformValueForGraph(rawValue, graph),
})) rawV: rawValue
.filter((point) => Number.isFinite(point.t) && Number.isFinite(point.v)) };
})
.filter((point) =>
Number.isFinite(point.t) &&
Number.isFinite(point.v) &&
Number.isFinite(point.rawV)
)
.sort((a, b) => a.t - b.t); .sort((a, b) => a.t - b.t);
if (points.length > maxPoints) { points = downsampleMinMax(points, maxPoints);
const originalLastPoint = points[points.length - 1];
const step = Math.ceil(points.length / maxPoints);
points = points.filter((_, index) => index % step === 0);
if (
originalLastPoint &&
points[points.length - 1]?.t !== originalLastPoint.t
) {
points.push(originalLastPoint);
}
}
return { return {
...graph, ...graph,
@@ -672,8 +942,14 @@ unique_bubble_graph:
return ""; return "";
})()} })()}
editor: editor:
- type: expandable
name: general_settings
title: Allgemein
icon: mdi:cog-outline
expanded: true
schema:
- name: hours_to_show - name: hours_to_show
label: Allgemein · Stunden anzeigen label: Stunden anzeigen
selector: selector:
number: number:
min: 1 min: 1
@@ -681,7 +957,7 @@ unique_bubble_graph:
step: 1 step: 1
mode: box mode: box
- name: refresh_minutes - name: refresh_minutes
label: Allgemein · Aktualisierung Minuten label: Aktualisierung Minuten
selector: selector:
number: number:
min: 1 min: 1
@@ -689,11 +965,11 @@ unique_bubble_graph:
step: 1 step: 1
mode: box mode: box
- name: persistent_cache - name: persistent_cache
label: Allgemein · Cache nach F5 behalten label: Cache nach F5 behalten
selector: selector:
boolean: null boolean: null
- name: max_cache_age_hours - name: max_cache_age_hours
label: Allgemein · Max. Cache-Alter Stunden label: Max. Cache-Alter Stunden
selector: selector:
number: number:
min: 1 min: 1
@@ -701,25 +977,58 @@ unique_bubble_graph:
step: 1 step: 1
mode: box mode: box
- name: show_loading - name: show_loading
label: Allgemein · Lade-Kringel anzeigen label: Lade-Kringel anzeigen
selector: selector:
boolean: null boolean: null
- type: expandable
name: scale_settings
title: Skalierung & Prozent-Umrechnung
icon: mdi:chart-line-variant
expanded: true
schema:
- name: same_scale
label: Gemeinsame Skalierung für alle Graphen
selector:
boolean: null
- name: same_scale_min
label: Gemeinsame Skala Minimum
selector:
number:
min: -100000
max: 100000
step: 1
mode: box
- name: same_scale_max
label: Gemeinsame Skala Maximum
selector:
number:
min: -100000
max: 100000
step: 1
mode: box
- type: expandable
name: graph_1_settings
title: ① Graph
icon: mdi:numeric-1-circle-outline
expanded: true
schema:
- name: graph_1_entity - name: graph_1_entity
label: ① Graph · Sensor label: Sensor
selector: selector:
entity: entity:
domain: sensor domain: sensor
- name: graph_1_label - name: graph_1_label
label: ① Graph · Label label: Label
selector: selector:
text: null text: null
- name: graph_1_color - name: graph_1_color
label: ① Graph · Farbe label: Farbe
selector: selector:
text: null color_rgb: null
- name: graph_1_line_width - name: graph_1_line_width
label: ① Graph · Linien Dicke label: Linien Dicke
selector: selector:
number: number:
min: 0.5 min: 0.5
@@ -727,29 +1036,52 @@ unique_bubble_graph:
step: 0.1 step: 0.1
mode: box mode: box
- name: graph_1_fill_opacity - name: graph_1_fill_opacity
label: ① Graph · Fläche label: Fläche
selector: selector:
number: number:
min: 0 min: 0
max: 1 max: 1
step: 0.05 step: 0.05
mode: slider mode: slider
- name: graph_1_value_mode
label: Wert-Modus
selector:
select:
options:
- label: Originalwert
value: default
- label: Prozent von Maximalwert
value: percent_of_max
- name: graph_1_max_value
label: Maximalwert für Prozent
selector:
number:
min: 0
max: 100000
step: 1
mode: box
- type: expandable
name: graph_2_settings
title: ② Graph
icon: mdi:numeric-2-circle-outline
expanded: false
schema:
- name: graph_2_entity - name: graph_2_entity
label: ② Graph · Sensor label: Sensor
selector: selector:
entity: entity:
domain: sensor domain: sensor
- name: graph_2_label - name: graph_2_label
label: ② Graph · Label label: Label
selector: selector:
text: null text: null
- name: graph_2_color - name: graph_2_color
label: ② Graph · Farbe label: Farbe
selector: selector:
text: null color_rgb: null
- name: graph_2_line_width - name: graph_2_line_width
label: ② Graph · Linien Dicke label: Linien Dicke
selector: selector:
number: number:
min: 0.5 min: 0.5
@@ -757,29 +1089,52 @@ unique_bubble_graph:
step: 0.1 step: 0.1
mode: box mode: box
- name: graph_2_fill_opacity - name: graph_2_fill_opacity
label: ② Graph · Fläche label: Fläche
selector: selector:
number: number:
min: 0 min: 0
max: 1 max: 1
step: 0.05 step: 0.05
mode: slider mode: slider
- name: graph_2_value_mode
label: Wert-Modus
selector:
select:
options:
- label: Originalwert
value: default
- label: Prozent von Maximalwert
value: percent_of_max
- name: graph_2_max_value
label: Maximalwert für Prozent
selector:
number:
min: 0
max: 100000
step: 1
mode: box
- type: expandable
name: graph_3_settings
title: ③ Graph
icon: mdi:numeric-3-circle-outline
expanded: false
schema:
- name: graph_3_entity - name: graph_3_entity
label: ③ Graph · Sensor label: Sensor
selector: selector:
entity: entity:
domain: sensor domain: sensor
- name: graph_3_label - name: graph_3_label
label: ③ Graph · Label label: Label
selector: selector:
text: null text: null
- name: graph_3_color - name: graph_3_color
label: ③ Graph · Farbe label: Farbe
selector: selector:
text: null color_rgb: null
- name: graph_3_line_width - name: graph_3_line_width
label: ③ Graph · Linien Dicke label: Linien Dicke
selector: selector:
number: number:
min: 0.5 min: 0.5
@@ -787,69 +1142,43 @@ unique_bubble_graph:
step: 0.1 step: 0.1
mode: box mode: box
- name: graph_3_fill_opacity - name: graph_3_fill_opacity
label: ③ Graph · Fläche label: Fläche
selector: selector:
number: number:
min: 0 min: 0
max: 1 max: 1
step: 0.05 step: 0.05
mode: slider mode: slider
- name: graph_3_value_mode
- name: line_width label: Wert-Modus
label: Anzeige · Standard Linien Dicke
selector: selector:
number: select:
min: 0.5 options:
max: 10 - label: Originalwert
step: 0.1 value: default
mode: box - label: Prozent von Maximalwert
- name: line_opacity value: percent_of_max
label: Anzeige · Linien Sichtbarkeit - name: graph_3_max_value
label: Maximalwert für Prozent
selector: selector:
number: number:
min: 0 min: 0
max: 1 max: 100000
step: 0.05
mode: slider
- name: fill_opacity
label: Anzeige · Standard Fläche
selector:
number:
min: 0
max: 1
step: 0.05
mode: slider
- name: value_padding_percent
label: Anzeige · Abstand oben/unten Prozent
selector:
number:
min: 0
max: 30
step: 1
mode: box
- name: padding_y
label: Anzeige · Graph Abstand oben/unten
selector:
number:
min: 0
max: 40
step: 1
mode: box
- name: border_radius
label: Anzeige · Eckenradius
selector:
number:
min: 0
max: 60
step: 1 step: 1
mode: box mode: box
- type: expandable
name: advanced_settings
title: Erweitert
icon: mdi:tune-variant
expanded: false
schema:
- name: extend_to_now - name: extend_to_now
label: Erweitert · Linie bis jetzt verlängern label: Linie bis jetzt verlängern
selector: selector:
boolean: null boolean: null
- name: extend_threshold_minutes - name: extend_threshold_minutes
label: Erweitert · Verlängern nach Minuten label: Verlängern nach Minuten
selector: selector:
number: number:
min: 0 min: 0
@@ -857,7 +1186,7 @@ unique_bubble_graph:
step: 1 step: 1
mode: box mode: box
- name: max_points - name: max_points
label: Erweitert · Max. Datenpunkte label: Max. Datenpunkte
selector: selector:
number: number:
min: 20 min: 20
@@ -865,11 +1194,11 @@ unique_bubble_graph:
step: 10 step: 10
mode: box mode: box
- name: tooltip - name: tooltip
label: Erweitert · Tooltip anzeigen label: Tooltip anzeigen
selector: selector:
boolean: null boolean: null
- name: tooltip_top - name: tooltip_top
label: Erweitert · Tooltip Position oben label: Tooltip Position oben
selector: selector:
number: number:
min: 0 min: 0