unique_bubble_graph.yaml hinzugefügt
This commit is contained in:
413
unique_bubble_graph.yaml
Normal file
413
unique_bubble_graph.yaml
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
unique_bubble_graph:
|
||||||
|
name: Unique Bubble History Background Graph
|
||||||
|
version: 1.3.1
|
||||||
|
creator: Torsten
|
||||||
|
supported:
|
||||||
|
- button
|
||||||
|
description: Shows a Home Assistant sensor history graph as Bubble Card background.
|
||||||
|
code: |
|
||||||
|
ha-card {
|
||||||
|
position: relative !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
border-radius: var(--bubble-history-border-radius, 28px) !important;
|
||||||
|
background: var(--ha-card-background, var(--card-background-color)) !important;
|
||||||
|
clip-path: inset(0 round var(--bubble-history-border-radius, 28px)) !important;
|
||||||
|
contain: paint !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-button-card-container,
|
||||||
|
.bubble-button-card,
|
||||||
|
.bubble-card-background {
|
||||||
|
background: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-history-background {
|
||||||
|
position: absolute !important;
|
||||||
|
inset: 0 !important;
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
z-index: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
border-radius: var(--bubble-history-border-radius, 28px) !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip-path: inset(0 round var(--bubble-history-border-radius, 28px)) !important;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-history-background svg {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-history-background path.area {
|
||||||
|
fill: var(--bubble-history-fill-color, var(--primary-color)) !important;
|
||||||
|
opacity: var(--bubble-history-fill-opacity, 0.10) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-history-background path.line {
|
||||||
|
fill: none !important;
|
||||||
|
stroke: var(--bubble-history-line-color, var(--primary-color)) !important;
|
||||||
|
stroke-width: var(--bubble-history-line-width, 2.5) !important;
|
||||||
|
stroke-linecap: round !important;
|
||||||
|
stroke-linejoin: round !important;
|
||||||
|
opacity: var(--bubble-history-line-opacity, 0.75) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-history-background path.loading-line {
|
||||||
|
opacity: 0.35 !important;
|
||||||
|
stroke-dasharray: 6 6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-history-tooltip {
|
||||||
|
position: absolute !important;
|
||||||
|
z-index: 10 !important;
|
||||||
|
display: none;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: rgba(20, 20, 20, 0.82);
|
||||||
|
color: white;
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.25;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-history-marker {
|
||||||
|
position: absolute !important;
|
||||||
|
z-index: 8 !important;
|
||||||
|
display: none;
|
||||||
|
top: 10%;
|
||||||
|
bottom: 10%;
|
||||||
|
width: 1px;
|
||||||
|
background: rgba(255, 255, 255, 0.55);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bubble-button-card-container,
|
||||||
|
.bubble-button-card,
|
||||||
|
.bubble-name-container,
|
||||||
|
.bubble-sub-button-container,
|
||||||
|
.bubble-icon-container,
|
||||||
|
.bubble-sub-button,
|
||||||
|
.bubble-icon,
|
||||||
|
.bubble-state,
|
||||||
|
.bubble-name {
|
||||||
|
position: relative !important;
|
||||||
|
z-index: 2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
${(() => {
|
||||||
|
const cfg =
|
||||||
|
this.config.unique_bubble_graph ||
|
||||||
|
this.config.history_background_graph ||
|
||||||
|
{};
|
||||||
|
|
||||||
|
const entity = cfg.entity || this.config.entity || "sensor.sma_tripower_x_pv_power";
|
||||||
|
|
||||||
|
const entityLabel =
|
||||||
|
cfg.label ||
|
||||||
|
hass.states?.[entity]?.attributes?.friendly_name ||
|
||||||
|
entity ||
|
||||||
|
"Sensor";
|
||||||
|
|
||||||
|
const unit = hass.states?.[entity]?.attributes?.unit_of_measurement || "";
|
||||||
|
|
||||||
|
const hoursToShow = Number(cfg.hours_to_show ?? 24);
|
||||||
|
const lineWidth = Number(cfg.line_width ?? 2.5);
|
||||||
|
const refreshMinutes = Number(cfg.refresh_minutes ?? 0);
|
||||||
|
const maxPoints = Number(cfg.max_points ?? 160);
|
||||||
|
const lineOpacity = Number(cfg.line_opacity ?? 0.75);
|
||||||
|
const fillOpacity = Number(cfg.fill_opacity ?? 0.10);
|
||||||
|
const borderRadius = Number(cfg.border_radius ?? 28);
|
||||||
|
const tooltipTop = Number(cfg.tooltip_top ?? 52);
|
||||||
|
const paddingY = Number(cfg.padding_y ?? 12);
|
||||||
|
|
||||||
|
const lineColor = cfg.line_color || "var(--primary-color)";
|
||||||
|
const fillColor = cfg.fill_color || lineColor;
|
||||||
|
|
||||||
|
const host = this.shadowRoot?.querySelector("ha-card")
|
||||||
|
|| this.querySelector?.("ha-card")
|
||||||
|
|| card?.querySelector?.("ha-card")
|
||||||
|
|| card;
|
||||||
|
|
||||||
|
if (!host || !entity) return "";
|
||||||
|
|
||||||
|
host.style.setProperty("--bubble-history-line-width", `${lineWidth}`);
|
||||||
|
host.style.setProperty("--bubble-history-line-opacity", `${lineOpacity}`);
|
||||||
|
host.style.setProperty("--bubble-history-fill-opacity", `${fillOpacity}`);
|
||||||
|
host.style.setProperty("--bubble-history-border-radius", `${borderRadius}px`);
|
||||||
|
host.style.setProperty("--bubble-history-line-color", lineColor);
|
||||||
|
host.style.setProperty("--bubble-history-fill-color", fillColor);
|
||||||
|
|
||||||
|
let bg = host.querySelector(".bubble-history-background");
|
||||||
|
let tooltip = host.querySelector(".bubble-history-tooltip");
|
||||||
|
let marker = host.querySelector(".bubble-history-marker");
|
||||||
|
|
||||||
|
if (!bg) {
|
||||||
|
bg = document.createElement("div");
|
||||||
|
bg.className = "bubble-history-background";
|
||||||
|
host.prepend(bg);
|
||||||
|
|
||||||
|
bg.innerHTML = `
|
||||||
|
<svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
||||||
|
<path class="line loading-line" d="M 0 70 L 20 55 L 40 65 L 60 45 L 80 58 L 100 38"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!tooltip) {
|
||||||
|
tooltip = document.createElement("div");
|
||||||
|
tooltip.className = "bubble-history-tooltip";
|
||||||
|
host.appendChild(tooltip);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!marker) {
|
||||||
|
marker = document.createElement("div");
|
||||||
|
marker.className = "bubble-history-marker";
|
||||||
|
host.appendChild(marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatValue = (value) => {
|
||||||
|
if (unit === "W" && Math.abs(value) >= 1000) {
|
||||||
|
return `${(value / 1000).toFixed(2)} kW`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Number.isInteger(value)) {
|
||||||
|
return `${value} ${unit}`.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${value.toFixed(1)} ${unit}`.trim();
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTime = (timestamp) => {
|
||||||
|
return new Date(timestamp).toLocaleString("de-DE", {
|
||||||
|
day: "2-digit",
|
||||||
|
month: "2-digit",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const attachTooltip = (points) => {
|
||||||
|
if (cfg.tooltip === false) return;
|
||||||
|
|
||||||
|
host.onmousemove = (event) => {
|
||||||
|
if (!points?.length) return;
|
||||||
|
|
||||||
|
const rect = host.getBoundingClientRect();
|
||||||
|
const xRatio = Math.min(1, Math.max(0, (event.clientX - rect.left) / rect.width));
|
||||||
|
|
||||||
|
const minTime = points[0].t;
|
||||||
|
const maxTime = points[points.length - 1].t;
|
||||||
|
const targetTime = minTime + xRatio * (maxTime - minTime);
|
||||||
|
|
||||||
|
let nearest = points[0];
|
||||||
|
let nearestDiff = Math.abs(points[0].t - targetTime);
|
||||||
|
|
||||||
|
for (const point of points) {
|
||||||
|
const diff = Math.abs(point.t - targetTime);
|
||||||
|
if (diff < nearestDiff) {
|
||||||
|
nearest = point;
|
||||||
|
nearestDiff = diff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const x = ((nearest.t - minTime) / (maxTime - minTime || 1)) * rect.width;
|
||||||
|
const safeX = Math.min(rect.width - 55, Math.max(55, x));
|
||||||
|
|
||||||
|
tooltip.innerHTML = `
|
||||||
|
<strong>${entityLabel}: ${formatValue(nearest.v)}</strong><br>
|
||||||
|
${formatTime(nearest.t)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
tooltip.style.display = "block";
|
||||||
|
marker.style.display = "block";
|
||||||
|
|
||||||
|
tooltip.style.left = `${safeX}px`;
|
||||||
|
tooltip.style.top = `${tooltipTop}px`;
|
||||||
|
marker.style.left = `${x}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
host.onmouseleave = () => {
|
||||||
|
tooltip.style.display = "none";
|
||||||
|
marker.style.display = "none";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hass?.states?.[entity]) {
|
||||||
|
console.warn(`Bubble history graph: entity not found: ${entity}`);
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hass?.fetchWithAuth) {
|
||||||
|
console.warn("Bubble history graph: hass.fetchWithAuth not available");
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bg.dataset.loading === "true") return "";
|
||||||
|
bg.dataset.loading = "true";
|
||||||
|
|
||||||
|
const end = new Date();
|
||||||
|
const start = new Date(end.getTime() - hoursToShow * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const url =
|
||||||
|
`/api/history/period/${encodeURIComponent(start.toISOString())}` +
|
||||||
|
`?filter_entity_id=${encodeURIComponent(entity)}` +
|
||||||
|
`&end_time=${encodeURIComponent(end.toISOString())}` +
|
||||||
|
`&minimal_response`;
|
||||||
|
|
||||||
|
hass.fetchWithAuth(url)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((history) => {
|
||||||
|
bg.dataset.loading = "false";
|
||||||
|
|
||||||
|
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))
|
||||||
|
.sort((a, b) => a.t - b.t);
|
||||||
|
|
||||||
|
if (points.length < 2) {
|
||||||
|
console.warn(`Bubble history graph: not enough points: ${points.length}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (points.length > maxPoints) {
|
||||||
|
const step = Math.ceil(points.length / maxPoints);
|
||||||
|
points = points.filter((_, index) => index % step === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = 100;
|
||||||
|
const height = 100;
|
||||||
|
|
||||||
|
const minTime = Math.min(...points.map((point) => point.t));
|
||||||
|
const maxTime = Math.max(...points.map((point) => point.t));
|
||||||
|
const minValue = Math.min(...points.map((point) => point.v));
|
||||||
|
const maxValue = Math.max(...points.map((point) => point.v));
|
||||||
|
|
||||||
|
const timeRange = maxTime - minTime || 1;
|
||||||
|
const valueRange = maxValue - minValue || 1;
|
||||||
|
|
||||||
|
const xy = points.map((point) => {
|
||||||
|
const x = ((point.t - minTime) / timeRange) * width;
|
||||||
|
const y =
|
||||||
|
height -
|
||||||
|
paddingY -
|
||||||
|
((point.v - minValue) / valueRange) *
|
||||||
|
(height - paddingY * 2);
|
||||||
|
|
||||||
|
return [x, y];
|
||||||
|
});
|
||||||
|
|
||||||
|
const linePath = xy
|
||||||
|
.map(([x, y], index) =>
|
||||||
|
`${index === 0 ? "M" : "L"} ${x.toFixed(2)} ${y.toFixed(2)}`
|
||||||
|
)
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
|
const areaPath = `${linePath} L ${width} ${height} L 0 ${height} Z`;
|
||||||
|
|
||||||
|
const svg = `
|
||||||
|
<svg viewBox="0 0 100 100" preserveAspectRatio="none" aria-hidden="true">
|
||||||
|
<path class="area" d="${areaPath}"></path>
|
||||||
|
<path class="line" d="${linePath}"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
bg.innerHTML = svg;
|
||||||
|
attachTooltip(points);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
bg.dataset.loading = "false";
|
||||||
|
console.warn("Bubble history graph error:", error);
|
||||||
|
});
|
||||||
|
|
||||||
|
return "";
|
||||||
|
})()}
|
||||||
|
editor:
|
||||||
|
- name: entity
|
||||||
|
label: Graph Sensor
|
||||||
|
selector:
|
||||||
|
entity:
|
||||||
|
domain: sensor
|
||||||
|
- name: label
|
||||||
|
label: Tooltip Label
|
||||||
|
selector:
|
||||||
|
text: null
|
||||||
|
- name: hours_to_show
|
||||||
|
label: Stunden anzeigen
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 1
|
||||||
|
max: 168
|
||||||
|
step: 1
|
||||||
|
mode: box
|
||||||
|
- name: line_width
|
||||||
|
label: Linien Dicke
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0.5
|
||||||
|
max: 10
|
||||||
|
step: 0.1
|
||||||
|
mode: box
|
||||||
|
- name: line_opacity
|
||||||
|
label: Linien Sichtbarkeit
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 1
|
||||||
|
step: 0.05
|
||||||
|
mode: slider
|
||||||
|
- name: fill_opacity
|
||||||
|
label: Flächen Sichtbarkeit
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 1
|
||||||
|
step: 0.05
|
||||||
|
mode: slider
|
||||||
|
- 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
|
||||||
|
- name: padding_y
|
||||||
|
label: Graph Abstand oben/unten
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 40
|
||||||
|
step: 1
|
||||||
|
mode: box
|
||||||
|
- name: border_radius
|
||||||
|
label: Eckenradius
|
||||||
|
selector:
|
||||||
|
number:
|
||||||
|
min: 0
|
||||||
|
max: 60
|
||||||
|
step: 1
|
||||||
|
mode: box
|
||||||
Reference in New Issue
Block a user