503 lines
12 KiB
HTML
503 lines
12 KiB
HTML
<div id="energy-app">
|
|
<div class="toolbar">
|
|
<label class="field">
|
|
<span>Zeitraum</span>
|
|
<select id="range">
|
|
<option value="day">Tag</option>
|
|
<option value="week">Woche</option>
|
|
<option value="month">Monat</option>
|
|
<option value="year">Jahr</option>
|
|
<option value="total">Gesamt</option>
|
|
<option value="custom">Datumsbereich</option>
|
|
</select>
|
|
</label>
|
|
|
|
<label id="date-field" class="field">
|
|
<span>Datum</span>
|
|
<input id="date" type="date">
|
|
</label>
|
|
|
|
<div id="navigation" class="button-row" aria-label="Zeitraum wechseln">
|
|
<button id="previous" class="icon-button" type="button" title="Vorheriger Zeitraum" aria-label="Vorheriger Zeitraum">‹</button>
|
|
<button id="today" type="button">Heute</button>
|
|
<button id="next" class="icon-button" type="button" title="Nächster Zeitraum" aria-label="Nächster Zeitraum">›</button>
|
|
</div>
|
|
|
|
<div id="custom-range" class="custom-range" hidden>
|
|
<label class="field">
|
|
<span>Von</span>
|
|
<input id="custom-start" type="date">
|
|
</label>
|
|
<label class="field">
|
|
<span>Bis</span>
|
|
<input id="custom-end" type="date">
|
|
</label>
|
|
<button id="apply-range" type="button">Anzeigen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="period" class="period" aria-live="polite"></div>
|
|
<div id="message" class="message" role="status" hidden></div>
|
|
|
|
<div id="summary" class="summary"></div>
|
|
|
|
<div id="charts" class="charts">
|
|
<section class="chart" aria-labelledby="self-consumption-title">
|
|
<h3 id="self-consumption-title">Eigenverbrauchsquote</h3>
|
|
<div id="self-consumption-chart" class="donut"></div>
|
|
</section>
|
|
<section class="chart" aria-labelledby="autarky-title">
|
|
<h3 id="autarky-title">Autarkiegrad</h3>
|
|
<div id="autarky-chart" class="donut"></div>
|
|
</section>
|
|
</div>
|
|
|
|
<div id="error" class="error" role="alert"></div>
|
|
</div>
|
|
|
|
<style>
|
|
:root {
|
|
color-scheme: light dark;
|
|
}
|
|
|
|
html,
|
|
body {
|
|
min-width: 0;
|
|
min-height: 100%;
|
|
margin: 0;
|
|
color: var(--content-color, inherit);
|
|
background-color: var(--card-color, transparent);
|
|
}
|
|
|
|
#energy-app {
|
|
width: 100%;
|
|
min-height: 100%;
|
|
box-sizing: border-box;
|
|
padding: 58px 14px 14px;
|
|
overflow: auto;
|
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
color: var(--content-color, inherit);
|
|
background-color: var(--card-color, transparent);
|
|
}
|
|
|
|
.toolbar,
|
|
.custom-range,
|
|
.button-row,
|
|
.summary {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: end;
|
|
gap: 10px;
|
|
}
|
|
|
|
.field {
|
|
display: grid;
|
|
min-width: 128px;
|
|
gap: 4px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
select,
|
|
input,
|
|
button {
|
|
min-height: 36px;
|
|
box-sizing: border-box;
|
|
border: 1px solid rgba(127, 127, 127, 0.45);
|
|
border-radius: 6px;
|
|
padding: 7px 10px;
|
|
color: var(--content-color, inherit);
|
|
background-color: var(--card-color, transparent);
|
|
font: inherit;
|
|
}
|
|
|
|
select option {
|
|
color: var(--content-color, CanvasText);
|
|
background-color: var(--card-color, Canvas);
|
|
}
|
|
|
|
button {
|
|
cursor: pointer;
|
|
font-weight: 650;
|
|
}
|
|
|
|
button:focus-visible,
|
|
select:focus-visible,
|
|
input:focus-visible {
|
|
outline: 2px solid currentColor;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.icon-button {
|
|
width: 38px;
|
|
padding: 0;
|
|
font-size: 25px;
|
|
line-height: 1;
|
|
}
|
|
|
|
.period {
|
|
min-height: 22px;
|
|
margin-top: 14px;
|
|
font-size: 15px;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.message {
|
|
margin-top: 10px;
|
|
padding: 10px 0;
|
|
border-top: 1px solid rgba(127, 127, 127, 0.35);
|
|
border-bottom: 1px solid rgba(127, 127, 127, 0.35);
|
|
}
|
|
|
|
.summary {
|
|
align-items: stretch;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.metric {
|
|
min-width: 130px;
|
|
flex: 1 1 150px;
|
|
padding: 8px 0;
|
|
border-bottom: 2px solid rgba(127, 127, 127, 0.32);
|
|
}
|
|
|
|
.metric span,
|
|
.metric strong {
|
|
display: block;
|
|
}
|
|
|
|
.metric span {
|
|
font-size: 12px;
|
|
opacity: 0.72;
|
|
}
|
|
|
|
.metric strong {
|
|
margin-top: 3px;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.charts {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, minmax(220px, 1fr));
|
|
gap: 22px;
|
|
align-items: start;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.charts.narrow {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
|
|
.chart {
|
|
min-width: 0;
|
|
text-align: center;
|
|
}
|
|
|
|
.chart h3 {
|
|
margin: 0 0 10px;
|
|
font-size: 15px;
|
|
letter-spacing: 0;
|
|
}
|
|
|
|
.donut {
|
|
width: 176px;
|
|
height: 176px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.donut svg {
|
|
display: block;
|
|
width: 176px;
|
|
height: 176px;
|
|
overflow: visible;
|
|
}
|
|
|
|
.donut-track {
|
|
stroke: rgba(127, 127, 127, 0.28);
|
|
}
|
|
|
|
.donut-value {
|
|
transition: stroke-dasharray 180ms ease;
|
|
}
|
|
|
|
.donut-label {
|
|
fill: currentColor;
|
|
font-size: 22px;
|
|
font-weight: 750;
|
|
text-anchor: middle;
|
|
dominant-baseline: middle;
|
|
}
|
|
|
|
.error {
|
|
min-height: 18px;
|
|
margin-top: 8px;
|
|
color: #d14343;
|
|
font-size: 13px;
|
|
}
|
|
|
|
[hidden] {
|
|
display: none !important;
|
|
}
|
|
|
|
@media (max-width: 560px) {
|
|
#energy-app {
|
|
padding: 58px 10px 10px;
|
|
}
|
|
|
|
.toolbar,
|
|
.custom-range {
|
|
align-items: stretch;
|
|
}
|
|
|
|
.field {
|
|
flex: 1 1 145px;
|
|
}
|
|
|
|
.charts {
|
|
grid-template-columns: minmax(0, 1fr);
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<script>
|
|
(function () {
|
|
'use strict';
|
|
|
|
const initialState = __ENELIX_INITIAL_STATE__;
|
|
const app = document.getElementById('energy-app');
|
|
const range = document.getElementById('range');
|
|
const date = document.getElementById('date');
|
|
const dateField = document.getElementById('date-field');
|
|
const navigation = document.getElementById('navigation');
|
|
const previous = document.getElementById('previous');
|
|
const today = document.getElementById('today');
|
|
const next = document.getElementById('next');
|
|
const customRange = document.getElementById('custom-range');
|
|
const customStart = document.getElementById('custom-start');
|
|
const customEnd = document.getElementById('custom-end');
|
|
const applyRange = document.getElementById('apply-range');
|
|
const period = document.getElementById('period');
|
|
const message = document.getElementById('message');
|
|
const summary = document.getElementById('summary');
|
|
const charts = document.getElementById('charts');
|
|
const error = document.getElementById('error');
|
|
const selfConsumptionChart = document.getElementById('self-consumption-chart');
|
|
const autarkyChart = document.getElementById('autarky-chart');
|
|
|
|
function syncColorScheme() {
|
|
const cardColor = getComputedStyle(document.documentElement)
|
|
.getPropertyValue('--card-color')
|
|
.trim();
|
|
const match = cardColor.match(/^#?([0-9a-f]{6})$/i);
|
|
if (!match) {
|
|
return;
|
|
}
|
|
|
|
const value = Number.parseInt(match[1], 16);
|
|
const red = (value >> 16) & 255;
|
|
const green = (value >> 8) & 255;
|
|
const blue = value & 255;
|
|
const luminance = ((0.2126 * red) + (0.7152 * green) + (0.0722 * blue)) / 255;
|
|
document.documentElement.style.colorScheme = luminance < 0.5 ? 'dark' : 'light';
|
|
}
|
|
|
|
function send(action, value) {
|
|
if (typeof requestAction !== 'function') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
requestAction(action, value);
|
|
error.textContent = '';
|
|
} catch (exception) {
|
|
error.textContent = exception && exception.message
|
|
? exception.message
|
|
: String(exception);
|
|
}
|
|
}
|
|
|
|
function asNumber(value) {
|
|
const parsed = Number(value);
|
|
return Number.isFinite(parsed) ? parsed : 0;
|
|
}
|
|
|
|
function formatEnergy(value) {
|
|
return asNumber(value).toLocaleString('de-CH', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}) + ' kWh';
|
|
}
|
|
|
|
function formatDate(value) {
|
|
const parts = String(value).split('-');
|
|
return parts.length === 3
|
|
? parts[2] + '.' + parts[1] + '.' + parts[0]
|
|
: '';
|
|
}
|
|
|
|
function periodLabel(data) {
|
|
if (data.range === 'total') {
|
|
return 'Gesamter Zählerstand';
|
|
}
|
|
if (data.range === 'custom') {
|
|
return formatDate(data.customStart) + ' bis ' + formatDate(data.customEnd);
|
|
}
|
|
|
|
const start = new Date(asNumber(data.tStart) * 1000);
|
|
const end = new Date((asNumber(data.tEnd) * 1000) - 1000);
|
|
if (data.range === 'day') {
|
|
return 'Tag: ' + start.toLocaleDateString('de-CH');
|
|
}
|
|
if (data.range === 'week') {
|
|
return 'Woche: ' + start.toLocaleDateString('de-CH')
|
|
+ ' bis ' + end.toLocaleDateString('de-CH');
|
|
}
|
|
if (data.range === 'month') {
|
|
return 'Monat: ' + start.toLocaleDateString('de-CH', {
|
|
month: 'long',
|
|
year: 'numeric'
|
|
});
|
|
}
|
|
if (data.range === 'year') {
|
|
return 'Jahr: ' + start.getFullYear();
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
function donut(target, percent, color) {
|
|
const value = Math.max(0, Math.min(100, asNumber(percent)));
|
|
const radius = 58;
|
|
const circumference = 2 * Math.PI * radius;
|
|
const filled = (value / 100) * circumference;
|
|
|
|
target.innerHTML = [
|
|
'<svg viewBox="0 0 176 176" role="img" aria-label="'
|
|
+ value.toFixed(1) + ' Prozent">',
|
|
'<circle class="donut-track" cx="88" cy="88" r="' + radius
|
|
+ '" fill="none" stroke-width="18"></circle>',
|
|
'<circle class="donut-value" cx="88" cy="88" r="' + radius
|
|
+ '" fill="none" stroke="' + color + '" stroke-width="18"'
|
|
+ ' stroke-linecap="round" transform="rotate(-90 88 88)"'
|
|
+ ' stroke-dasharray="' + filled + ' ' + circumference + '"></circle>',
|
|
'<text class="donut-label" x="88" y="91">'
|
|
+ value.toFixed(1) + '%</text>',
|
|
'</svg>'
|
|
].join('');
|
|
}
|
|
|
|
function metric(label, value) {
|
|
return '<div class="metric"><span>' + label + '</span><strong>'
|
|
+ formatEnergy(value) + '</strong></div>';
|
|
}
|
|
|
|
function render(data) {
|
|
if (!data || typeof data !== 'object') {
|
|
return;
|
|
}
|
|
|
|
syncColorScheme();
|
|
range.value = data.range || 'day';
|
|
date.value = data.date || '';
|
|
customStart.value = data.customStart || '';
|
|
customEnd.value = data.customEnd || '';
|
|
const currentMode = range.value;
|
|
const custom = currentMode === 'custom';
|
|
const total = currentMode === 'total';
|
|
|
|
dateField.hidden = custom || total;
|
|
navigation.hidden = custom || total;
|
|
customRange.hidden = !custom;
|
|
period.textContent = periodLabel(data);
|
|
|
|
const values = data.values || {};
|
|
summary.innerHTML = [
|
|
metric('Produktion', values.Produktion),
|
|
metric('Hausverbrauch', values.Hausverbrauch),
|
|
metric('Netzbezug', values.Netzbezug),
|
|
metric('Einspeisung', values.Einspeisung)
|
|
].join('');
|
|
|
|
const colors = data.colors || {};
|
|
donut(
|
|
selfConsumptionChart,
|
|
values.Eigenverbrauchsquote,
|
|
colors.selfConsumption || '#2F80ED'
|
|
);
|
|
donut(
|
|
autarkyChart,
|
|
values.Autarkiegrad,
|
|
colors.autarky || '#22A06B'
|
|
);
|
|
|
|
message.hidden = data.hasData !== false;
|
|
message.textContent = data.hasData === false
|
|
? (data.noDataHint || 'Keine Archivwerte verfügbar.')
|
|
: '';
|
|
}
|
|
|
|
window.handleMessage = function (data) {
|
|
if (typeof data === 'string') {
|
|
try {
|
|
data = JSON.parse(data);
|
|
} catch (exception) {
|
|
error.textContent = 'Die Aktualisierung enthält ungültige Daten.';
|
|
return;
|
|
}
|
|
}
|
|
render(data);
|
|
};
|
|
|
|
range.addEventListener('change', function () {
|
|
send('SetRange', range.value);
|
|
});
|
|
date.addEventListener('change', function () {
|
|
send('SetDate', date.value);
|
|
});
|
|
previous.addEventListener('click', function () {
|
|
send('Prev', true);
|
|
});
|
|
next.addEventListener('click', function () {
|
|
send('Next', true);
|
|
});
|
|
today.addEventListener('click', function () {
|
|
send('Today', true);
|
|
});
|
|
applyRange.addEventListener('click', function () {
|
|
send('SetCustomRange', JSON.stringify({
|
|
start: customStart.value,
|
|
end: customEnd.value
|
|
}));
|
|
});
|
|
|
|
const now = new Date();
|
|
const currentDate = [
|
|
now.getFullYear(),
|
|
String(now.getMonth() + 1).padStart(2, '0'),
|
|
String(now.getDate()).padStart(2, '0')
|
|
].join('-');
|
|
date.max = currentDate;
|
|
customStart.max = currentDate;
|
|
customEnd.max = currentDate;
|
|
|
|
if (typeof ResizeObserver === 'function') {
|
|
const observer = new ResizeObserver(function (entries) {
|
|
const width = entries[0] ? entries[0].contentRect.width : app.clientWidth;
|
|
charts.classList.toggle('narrow', width < 520);
|
|
});
|
|
observer.observe(app);
|
|
}
|
|
|
|
document.addEventListener('visibilitychange', function () {
|
|
if (!document.hidden) {
|
|
send('Refresh', true);
|
|
}
|
|
});
|
|
|
|
render(initialState);
|
|
window.setTimeout(function () {
|
|
send('Refresh', true);
|
|
}, 0);
|
|
})();
|
|
</script>
|