diff --git a/configuration.yaml b/configuration.yaml index ea10d56..363c470 100755 --- a/configuration.yaml +++ b/configuration.yaml @@ -140,6 +140,8 @@ lovelace: type: module - url: /hacsfiles/custom-gauge-card/custom-gauge-card.js type: module + - url: /local/community/apexcharts-card/apexcharts-card.js + type: module dashboards: lovelace: mode: yaml diff --git a/dashboards/views/01_home.yaml b/dashboards/views/01_home.yaml index 0b22c49..8ea1a7e 100644 --- a/dashboards/views/01_home.yaml +++ b/dashboards/views/01_home.yaml @@ -295,17 +295,138 @@ cards: red: 80 - # ⚡ Energi + 🍽️ Opvaskemaskine (kompakt) - - type: horizontal-stack + # ⚡ 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); - - type: tile - entity: sensor.dishwasher_next_start_compact - name: Næste opvask + const rawToday = entity.attributes.raw_today || []; + const rawTomorrow = entity.attributes.tomorrow_valid ? (entity.attributes.raw_tomorrow || []) : []; + const forecast = entity.attributes.forecast || []; - - type: tile - entity: sensor.opvask_tid_tilbage - name: Tid tilbage + 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 + cards: + - type: tile + entity: sensor.dishwasher_next_start_compact + name: Næste opvask + + - type: tile + entity: sensor.opvask_tid_tilbage + name: Tid tilbage # 🧹 Støvsuger (forenklet – mindre støj) - type: entities