diff --git a/unique_bubble_graph.yaml b/unique_bubble_graph.yaml
index 47dd434..09a4458 100644
--- a/unique_bubble_graph.yaml
+++ b/unique_bubble_graph.yaml
@@ -1,10 +1,10 @@
unique_bubble_graph:
name: Unique Bubble Multi History Background Graph
- version: 2.6.0
+ version: 2.8.6
creator: Torsten
supported:
- 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: |
ha-card {
position: relative !important;
@@ -134,11 +134,34 @@ unique_bubble_graph:
}
${(() => {
- const cfg =
+ const rawCfg =
this.config.unique_bubble_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 refreshMinutes = Number(cfg.refresh_minutes ?? 10);
const maxPoints = Number(cfg.max_points ?? 120);
@@ -153,80 +176,172 @@ unique_bubble_graph:
const extendToNow = cfg.extend_to_now !== false;
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 defaultLineOpacity = Number(cfg.line_opacity ?? 0.75);
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 = [
- cfg.graph_1_color || cfg.line_color || "var(--primary-color)",
- cfg.graph_2_color || "#2196f3",
- cfg.graph_3_color || "#4caf50"
+ "var(--primary-color)",
+ "#2196f3",
+ "#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 = () => {
if (Array.isArray(cfg.graphs) && cfg.graphs.length) {
return cfg.graphs
.slice(0, 3)
- .map((graph, index) => ({
+ .map((graph, index) => {
+ const scaleConfig = getGraphScaleConfig(graph, index);
+
+ return {
entity: graph.entity,
label:
graph.label ||
hass.states?.[graph.entity]?.attributes?.friendly_name ||
graph.entity ||
`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),
lineOpacity: Number(graph.line_opacity ?? defaultLineOpacity),
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);
}
- const graph1Entity = cfg.graph_1_entity || cfg.entity || this.config.entity || "sensor.sma_tripower_x_pv_power";
- const graph2Entity = cfg.graph_2_entity;
- const graph3Entity = cfg.graph_3_entity;
+ const graph1Entity = getGraphSetting(0, "entity");
+ const graph2Entity = getGraphSetting(1, "entity");
+ const graph3Entity = getGraphSetting(2, "entity");
return [
{
entity: graph1Entity,
label:
- cfg.graph_1_label ||
+ getGraphSetting(0, "label") ||
cfg.label ||
hass.states?.[graph1Entity]?.attributes?.friendly_name ||
graph1Entity ||
"Graph 1",
- color: cfg.graph_1_color || cfg.line_color || defaultColors[0],
- lineWidth: Number(cfg.graph_1_line_width ?? defaultLineWidth),
- lineOpacity: Number(cfg.graph_1_line_opacity ?? defaultLineOpacity),
- fillOpacity: Number(cfg.graph_1_fill_opacity ?? defaultFillOpacity),
- unit: hass.states?.[graph1Entity]?.attributes?.unit_of_measurement || ""
+ color: normalizeColor(getGraphSetting(0, "color") || cfg.line_color, defaultColors[0]),
+ lineWidth: Number(getGraphSetting(0, "line_width", defaultLineWidth)),
+ lineOpacity: Number(getGraphSetting(0, "line_opacity", defaultLineOpacity)),
+ fillOpacity: Number(getGraphSetting(0, "fill_opacity", defaultFillOpacity)),
+ 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,
label:
- cfg.graph_2_label ||
+ getGraphSetting(1, "label") ||
hass.states?.[graph2Entity]?.attributes?.friendly_name ||
graph2Entity ||
"Graph 2",
- color: cfg.graph_2_color || defaultColors[1],
- lineWidth: Number(cfg.graph_2_line_width ?? defaultLineWidth),
- lineOpacity: Number(cfg.graph_2_line_opacity ?? defaultLineOpacity),
- fillOpacity: Number(cfg.graph_2_fill_opacity ?? defaultFillOpacity),
- unit: hass.states?.[graph2Entity]?.attributes?.unit_of_measurement || ""
+ color: normalizeColor(getGraphSetting(1, "color"), defaultColors[1]),
+ lineWidth: Number(getGraphSetting(1, "line_width", defaultLineWidth)),
+ lineOpacity: Number(getGraphSetting(1, "line_opacity", defaultLineOpacity)),
+ fillOpacity: Number(getGraphSetting(1, "fill_opacity", defaultFillOpacity)),
+ 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,
label:
- cfg.graph_3_label ||
+ getGraphSetting(2, "label") ||
hass.states?.[graph3Entity]?.attributes?.friendly_name ||
graph3Entity ||
"Graph 3",
- color: cfg.graph_3_color || defaultColors[2],
- lineWidth: Number(cfg.graph_3_line_width ?? defaultLineWidth),
- lineOpacity: Number(cfg.graph_3_line_opacity ?? defaultLineOpacity),
- fillOpacity: Number(cfg.graph_3_fill_opacity ?? defaultFillOpacity),
- unit: hass.states?.[graph3Entity]?.attributes?.unit_of_measurement || ""
+ color: normalizeColor(getGraphSetting(2, "color"), defaultColors[2]),
+ lineWidth: Number(getGraphSetting(2, "line_width", defaultLineWidth)),
+ lineOpacity: Number(getGraphSetting(2, "line_opacity", defaultLineOpacity)),
+ fillOpacity: Number(getGraphSetting(2, "fill_opacity", defaultFillOpacity)),
+ 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);
};
@@ -299,6 +414,31 @@ unique_bubble_graph:
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) => {
return new Date(timestamp).toLocaleString("de-DE", {
day: "2-digit",
@@ -325,6 +465,42 @@ unique_bubble_graph:
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) => {
if (!extendToNow) return seriesList;
@@ -334,15 +510,18 @@ unique_bubble_graph:
return seriesList.map((series) => {
const points = [...(series.points || [])];
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 (
+ Number.isFinite(currentRawValue) &&
Number.isFinite(currentValue) &&
(!lastPoint || now - lastPoint.t > thresholdMs)
) {
points.push({
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;
+ host.__bubbleHistoryTooltip = tooltip;
+ host.__bubbleHistoryMarker = marker;
+ host.__bubbleHistoryTooltipTop = tooltipTop;
+
+ if (host.__bubbleHistoryTooltipAttached) return;
+
+ host.__bubbleHistoryTooltipAttached = true;
+
host.onmousemove = (event) => {
- const usableSeries = seriesList.filter((series) => series.points?.length);
+ const usableSeries = (host.__bubbleHistorySeries || [])
+ .filter((series) => series.points?.length);
+
if (!usableSeries.length) return;
+ const tooltipElement = host.__bubbleHistoryTooltip;
+ const markerElement = host.__bubbleHistoryMarker;
+
+ if (!tooltipElement || !markerElement) return;
+
const rect = host.getBoundingClientRect();
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 globalMaxTime = Math.max(...usableSeries.map((series) => series.points[series.points.length - 1].t));
+ const globalMinTime =
+ 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 rows = usableSeries.map((series) => {
@@ -374,7 +576,7 @@ unique_bubble_graph:
return `
- ${series.label}: ${formatValue(nearest.v, series.unit)}
+ ${series.label}: ${formatTooltipValue(nearest, series)}
`;
}).join("");
@@ -385,22 +587,48 @@ unique_bubble_graph:
const x = ((targetTime - globalMinTime) / (globalMaxTime - globalMinTime || 1)) * rect.width;
const safeX = Math.min(rect.width - 60, Math.max(60, x));
- tooltip.innerHTML = `
+ tooltipElement.innerHTML = `
${rows}
${formatTime(tooltipTime)}
`;
- tooltip.style.display = "block";
- marker.style.display = "block";
+ tooltipElement.style.display = "block";
+ markerElement.style.display = "block";
- tooltip.style.left = `${safeX}px`;
- tooltip.style.top = `${tooltipTop}px`;
- marker.style.left = `${x}px`;
+ tooltipElement.style.left = `${safeX}px`;
+ tooltipElement.style.top = `${host.__bubbleHistoryTooltipTop}px`;
+ markerElement.style.left = `${x}px`;
};
host.onmouseleave = () => {
- tooltip.style.display = "none";
- marker.style.display = "none";
+ const tooltipElement = host.__bubbleHistoryTooltip;
+ 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,17 +643,26 @@ unique_bubble_graph:
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 sameScaleRange = sameScale ? getSameScaleRange(usableSeries) : null;
const buildPath = (series) => {
- const rawMinValue = Math.min(...series.points.map((point) => point.v));
- const rawMaxValue = Math.max(...series.points.map((point) => point.v));
+ let minValue;
+ let maxValue;
- const rawRange = rawMaxValue - rawMinValue;
- const safeRange = rawRange || Math.max(Math.abs(rawMaxValue), 1);
- const valuePadding = safeRange * (valuePaddingPercent / 100);
+ if (sameScaleRange) {
+ minValue = sameScaleRange.minValue;
+ maxValue = sameScaleRange.maxValue;
+ } else {
+ const rawMinValue = Math.min(...series.points.map((point) => point.v));
+ const rawMaxValue = Math.max(...series.points.map((point) => point.v));
- const minValue = rawMinValue - valuePadding;
- const maxValue = rawMaxValue + valuePadding;
+ const rawRange = rawMaxValue - rawMinValue;
+ const safeRange = rawRange || Math.max(Math.abs(rawMaxValue), 1);
+ const valuePadding = safeRange * (valuePaddingPercent / 100);
+
+ minValue = rawMinValue - valuePadding;
+ maxValue = rawMaxValue + valuePadding;
+ }
const timeRange = globalMaxTime - globalMinTime || 1;
const valueRange = maxValue - minValue || 1;
@@ -477,7 +714,10 @@ unique_bubble_graph:
`;
- attachTooltip(usableSeries);
+ host.__bubbleHistorySeries = usableSeries;
+ host.__bubbleHistoryMinTime = globalMinTime;
+ host.__bubbleHistoryMaxTime = globalMaxTime;
+ attachTooltipOnce();
return true;
};
@@ -500,13 +740,41 @@ unique_bubble_graph:
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 = [
- "unique-bubble-graph-data-v260",
+ "unique-bubble-graph-data-v285",
dataCacheParts,
hoursToShow,
- maxPoints
+ maxPoints,
+ sameScale ? "same-scale" : "individual-scale",
+ sameScaleMin ?? "auto-min",
+ sameScaleMax ?? "auto-max"
].join("::");
const storageKey = `unique-bubble-graph-data-cache:${cacheKey}`;
@@ -558,6 +826,11 @@ unique_bubble_graph:
readPersistentCache();
if (cache?.seriesList) {
+ cache = {
+ ...cache,
+ seriesList: applyCurrentGraphSettingsToSeries(cache.seriesList)
+ };
+
renderGraph(cache.seriesList);
window.__uniqueBubbleGraphDataCache[cacheKey] = {
@@ -600,26 +873,23 @@ unique_bubble_graph:
const states = Array.isArray(history) ? history.flat() : [];
let points = states
- .map((state) => ({
- t: new Date(state.lu || state.lc || state.last_updated || state.last_changed).getTime(),
- v: Number(state.s ?? state.state)
- }))
- .filter((point) => Number.isFinite(point.t) && Number.isFinite(point.v))
+ .map((state) => {
+ const rawValue = Number(state.s ?? state.state);
+
+ return {
+ t: new Date(state.lu || state.lc || state.last_updated || state.last_changed).getTime(),
+ v: transformValueForGraph(rawValue, graph),
+ rawV: rawValue
+ };
+ })
+ .filter((point) =>
+ Number.isFinite(point.t) &&
+ Number.isFinite(point.v) &&
+ Number.isFinite(point.rawV)
+ )
.sort((a, b) => a.t - b.t);
- if (points.length > 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);
- }
- }
+ points = downsampleMinMax(points, maxPoints);
return {
...graph,
@@ -672,207 +942,266 @@ unique_bubble_graph:
return "";
})()}
editor:
- - name: hours_to_show
- label: Allgemein · Stunden anzeigen
- selector:
- number:
- min: 1
- max: 168
- step: 1
- mode: box
- - name: refresh_minutes
- label: Allgemein · Aktualisierung Minuten
- selector:
- number:
- min: 1
- max: 120
- step: 1
- mode: box
- - name: persistent_cache
- label: Allgemein · Cache nach F5 behalten
- selector:
- boolean: null
- - name: max_cache_age_hours
- label: Allgemein · Max. Cache-Alter Stunden
- selector:
- number:
- min: 1
- max: 168
- step: 1
- mode: box
- - name: show_loading
- label: Allgemein · Lade-Kringel anzeigen
- selector:
- boolean: null
+ - type: expandable
+ name: general_settings
+ title: Allgemein
+ icon: mdi:cog-outline
+ expanded: true
+ schema:
+ - name: hours_to_show
+ label: Stunden anzeigen
+ selector:
+ number:
+ min: 1
+ max: 168
+ step: 1
+ mode: box
+ - name: refresh_minutes
+ label: Aktualisierung Minuten
+ selector:
+ number:
+ min: 1
+ max: 120
+ step: 1
+ mode: box
+ - name: persistent_cache
+ label: Cache nach F5 behalten
+ selector:
+ boolean: null
+ - name: max_cache_age_hours
+ label: Max. Cache-Alter Stunden
+ selector:
+ number:
+ min: 1
+ max: 168
+ step: 1
+ mode: box
+ - name: show_loading
+ label: Lade-Kringel anzeigen
+ selector:
+ boolean: null
- - name: graph_1_entity
- label: ① Graph · Sensor
- selector:
- entity:
- domain: sensor
- - name: graph_1_label
- label: ① Graph · Label
- selector:
- text: null
- - name: graph_1_color
- label: ① Graph · Farbe
- selector:
- text: null
- - name: graph_1_line_width
- label: ① Graph · Linien Dicke
- selector:
- number:
- min: 0.5
- max: 10
- step: 0.1
- mode: box
- - name: graph_1_fill_opacity
- label: ① Graph · Fläche
- selector:
- number:
- min: 0
- max: 1
- step: 0.05
- mode: slider
+ - 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
- - name: graph_2_entity
- label: ② Graph · Sensor
- selector:
- entity:
- domain: sensor
- - name: graph_2_label
- label: ② Graph · Label
- selector:
- text: null
- - name: graph_2_color
- label: ② Graph · Farbe
- selector:
- text: null
- - name: graph_2_line_width
- label: ② Graph · Linien Dicke
- selector:
- number:
- min: 0.5
- max: 10
- step: 0.1
- mode: box
- - name: graph_2_fill_opacity
- label: ② Graph · Fläche
- selector:
- number:
- min: 0
- max: 1
- step: 0.05
- mode: slider
+ - type: expandable
+ name: graph_1_settings
+ title: ① Graph
+ icon: mdi:numeric-1-circle-outline
+ expanded: true
+ schema:
+ - name: graph_1_entity
+ label: Sensor
+ selector:
+ entity:
+ domain: sensor
+ - name: graph_1_label
+ label: Label
+ selector:
+ text: null
+ - name: graph_1_color
+ label: Farbe
+ selector:
+ color_rgb: null
+ - name: graph_1_line_width
+ label: Linien Dicke
+ selector:
+ number:
+ min: 0.5
+ max: 10
+ step: 0.1
+ mode: box
+ - name: graph_1_fill_opacity
+ label: Fläche
+ selector:
+ number:
+ min: 0
+ max: 1
+ step: 0.05
+ 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
- - name: graph_3_entity
- label: ③ Graph · Sensor
- selector:
- entity:
- domain: sensor
- - name: graph_3_label
- label: ③ Graph · Label
- selector:
- text: null
- - name: graph_3_color
- label: ③ Graph · Farbe
- selector:
- text: null
- - name: graph_3_line_width
- label: ③ Graph · Linien Dicke
- selector:
- number:
- min: 0.5
- max: 10
- step: 0.1
- mode: box
- - name: graph_3_fill_opacity
- label: ③ Graph · Fläche
- selector:
- number:
- min: 0
- max: 1
- step: 0.05
- mode: slider
+ - type: expandable
+ name: graph_2_settings
+ title: ② Graph
+ icon: mdi:numeric-2-circle-outline
+ expanded: false
+ schema:
+ - name: graph_2_entity
+ label: Sensor
+ selector:
+ entity:
+ domain: sensor
+ - name: graph_2_label
+ label: Label
+ selector:
+ text: null
+ - name: graph_2_color
+ label: Farbe
+ selector:
+ color_rgb: null
+ - name: graph_2_line_width
+ label: Linien Dicke
+ selector:
+ number:
+ min: 0.5
+ max: 10
+ step: 0.1
+ mode: box
+ - name: graph_2_fill_opacity
+ label: Fläche
+ selector:
+ number:
+ min: 0
+ max: 1
+ step: 0.05
+ 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
- - name: line_width
- label: Anzeige · Standard Linien Dicke
- selector:
- number:
- min: 0.5
- max: 10
- step: 0.1
- mode: box
- - name: line_opacity
- label: Anzeige · Linien Sichtbarkeit
- selector:
- number:
- min: 0
- max: 1
- 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
- mode: box
+ - type: expandable
+ name: graph_3_settings
+ title: ③ Graph
+ icon: mdi:numeric-3-circle-outline
+ expanded: false
+ schema:
+ - name: graph_3_entity
+ label: Sensor
+ selector:
+ entity:
+ domain: sensor
+ - name: graph_3_label
+ label: Label
+ selector:
+ text: null
+ - name: graph_3_color
+ label: Farbe
+ selector:
+ color_rgb: null
+ - name: graph_3_line_width
+ label: Linien Dicke
+ selector:
+ number:
+ min: 0.5
+ max: 10
+ step: 0.1
+ mode: box
+ - name: graph_3_fill_opacity
+ label: Fläche
+ selector:
+ number:
+ min: 0
+ max: 1
+ step: 0.05
+ mode: slider
+ - name: graph_3_value_mode
+ label: Wert-Modus
+ selector:
+ select:
+ options:
+ - label: Originalwert
+ value: default
+ - label: Prozent von Maximalwert
+ value: percent_of_max
+ - name: graph_3_max_value
+ label: Maximalwert für Prozent
+ selector:
+ number:
+ min: 0
+ max: 100000
+ step: 1
+ mode: box
- - name: extend_to_now
- label: Erweitert · Linie bis jetzt verlängern
- selector:
- boolean: null
- - name: extend_threshold_minutes
- label: Erweitert · Verlängern nach Minuten
- selector:
- number:
- min: 0
- max: 60
- step: 1
- mode: box
- - name: max_points
- label: Erweitert · Max. Datenpunkte
- selector:
- number:
- min: 20
- max: 500
- step: 10
- mode: box
- - name: tooltip
- label: Erweitert · Tooltip anzeigen
- selector:
- boolean: null
- - name: tooltip_top
- label: Erweitert · Tooltip Position oben
- selector:
- number:
- min: 0
- max: 200
- step: 1
- mode: box
+ - type: expandable
+ name: advanced_settings
+ title: Erweitert
+ icon: mdi:tune-variant
+ expanded: false
+ schema:
+ - name: extend_to_now
+ label: Linie bis jetzt verlängern
+ selector:
+ boolean: null
+ - name: extend_threshold_minutes
+ label: Verlängern nach Minuten
+ selector:
+ number:
+ min: 0
+ max: 60
+ step: 1
+ mode: box
+ - name: max_points
+ label: Max. Datenpunkte
+ selector:
+ number:
+ min: 20
+ max: 500
+ step: 10
+ mode: box
+ - name: tooltip
+ label: Tooltip anzeigen
+ selector:
+ boolean: null
+ - name: tooltip_top
+ label: Tooltip Position oben
+ selector:
+ number:
+ min: 0
+ max: 200
+ step: 1
+ mode: box