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