Add hourly electricity price chart
This commit is contained in:
@@ -140,6 +140,8 @@ lovelace:
|
|||||||
type: module
|
type: module
|
||||||
- url: /hacsfiles/custom-gauge-card/custom-gauge-card.js
|
- url: /hacsfiles/custom-gauge-card/custom-gauge-card.js
|
||||||
type: module
|
type: module
|
||||||
|
- url: /local/community/apexcharts-card/apexcharts-card.js
|
||||||
|
type: module
|
||||||
dashboards:
|
dashboards:
|
||||||
lovelace:
|
lovelace:
|
||||||
mode: yaml
|
mode: yaml
|
||||||
|
|||||||
@@ -295,10 +295,131 @@ cards:
|
|||||||
red: 80
|
red: 80
|
||||||
|
|
||||||
|
|
||||||
# ⚡ Energi + 🍽️ Opvaskemaskine (kompakt)
|
# ⚡ El-priser + 🍽️ Opvaskemaskine
|
||||||
|
- type: vertical-stack
|
||||||
|
cards:
|
||||||
|
- type: custom:apexcharts-card
|
||||||
|
graph_span: 24h
|
||||||
|
span:
|
||||||
|
start: hour
|
||||||
|
stacked: false
|
||||||
|
header:
|
||||||
|
show: true
|
||||||
|
title: El-priser næste 24 timer
|
||||||
|
show_states: true
|
||||||
|
colorize_states: true
|
||||||
|
now:
|
||||||
|
show: true
|
||||||
|
label: Nu
|
||||||
|
all_series_config:
|
||||||
|
stroke_width: 0
|
||||||
|
apex_config:
|
||||||
|
chart:
|
||||||
|
height: 260
|
||||||
|
grid:
|
||||||
|
strokeDashArray: 2
|
||||||
|
xaxis:
|
||||||
|
type: datetime
|
||||||
|
labels:
|
||||||
|
datetimeFormatter:
|
||||||
|
hour: HH:mm
|
||||||
|
yaxis:
|
||||||
|
decimalsInFloat: 2
|
||||||
|
tickAmount: 5
|
||||||
|
plotOptions:
|
||||||
|
bar:
|
||||||
|
columnWidth: 82%
|
||||||
|
borderRadius: 3
|
||||||
|
series:
|
||||||
|
- entity: sensor.energi_data_service
|
||||||
|
name: Pris
|
||||||
|
type: column
|
||||||
|
float_precision: 2
|
||||||
|
unit: ' kr/kWh'
|
||||||
|
show:
|
||||||
|
in_header: raw
|
||||||
|
in_chart: true
|
||||||
|
data_generator: |
|
||||||
|
const startOfHour = new Date();
|
||||||
|
startOfHour.setMinutes(0, 0, 0);
|
||||||
|
const endTime = startOfHour.getTime() + (24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
const rawToday = entity.attributes.raw_today || [];
|
||||||
|
const rawTomorrow = entity.attributes.tomorrow_valid ? (entity.attributes.raw_tomorrow || []) : [];
|
||||||
|
const forecast = entity.attributes.forecast || [];
|
||||||
|
|
||||||
|
const allKnown = [...rawToday, ...rawTomorrow];
|
||||||
|
const data = [];
|
||||||
|
const seen = new Set();
|
||||||
|
|
||||||
|
const pushPoint = (item) => {
|
||||||
|
const timestamp = new Date(item.hour).getTime();
|
||||||
|
if (Number.isNaN(timestamp) || timestamp < startOfHour.getTime() || timestamp >= endTime || seen.has(timestamp)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const price = Number(item.price);
|
||||||
|
if (Number.isNaN(price)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(timestamp);
|
||||||
|
data.push({ x: timestamp, y: price });
|
||||||
|
};
|
||||||
|
|
||||||
|
allKnown.forEach(pushPoint);
|
||||||
|
|
||||||
|
if (data.length < 24) {
|
||||||
|
forecast.forEach(pushPoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.sort((left, right) => left.x - right.x);
|
||||||
|
|
||||||
|
const trimmed = data.slice(0, 24);
|
||||||
|
if (!trimmed.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const prices = trimmed.map((item) => item.y);
|
||||||
|
const minPrice = Math.min(...prices);
|
||||||
|
const maxPrice = Math.max(...prices);
|
||||||
|
|
||||||
|
const mix = (start, end, ratio) => Math.round(start + ((end - start) * ratio));
|
||||||
|
const toHex = (value) => value.toString(16).padStart(2, '0');
|
||||||
|
const rgbToHex = (red, green, blue) => `#${toHex(red)}${toHex(green)}${toHex(blue)}`;
|
||||||
|
|
||||||
|
const colorByValue = (value) => {
|
||||||
|
if (maxPrice === minPrice) {
|
||||||
|
return '#16a34a';
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = (value - minPrice) / (maxPrice - minPrice);
|
||||||
|
|
||||||
|
if (normalized <= 0.5) {
|
||||||
|
const ratio = normalized / 0.5;
|
||||||
|
return rgbToHex(
|
||||||
|
mix(22, 250, ratio),
|
||||||
|
mix(163, 204, ratio),
|
||||||
|
mix(74, 21, ratio)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ratio = (normalized - 0.5) / 0.5;
|
||||||
|
return rgbToHex(
|
||||||
|
mix(250, 220, ratio),
|
||||||
|
mix(204, 38, ratio),
|
||||||
|
mix(21, 38, ratio)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return trimmed.map((item) => ({
|
||||||
|
x: item.x,
|
||||||
|
y: item.y,
|
||||||
|
fillColor: colorByValue(item.y)
|
||||||
|
}));
|
||||||
|
|
||||||
- type: horizontal-stack
|
- type: horizontal-stack
|
||||||
cards:
|
cards:
|
||||||
|
|
||||||
- type: tile
|
- type: tile
|
||||||
entity: sensor.dishwasher_next_start_compact
|
entity: sensor.dishwasher_next_start_compact
|
||||||
name: Næste opvask
|
name: Næste opvask
|
||||||
|
|||||||
Reference in New Issue
Block a user