solar W
grid in W
@@ -806,6 +937,82 @@ function renderShelly(devices) {
+ '';
}
+const CAUSE_LABEL = {
+ rule: 'rule',
+ floor: 'safety floor',
+ stale: 'telemetry lost',
+ recovered: 'recovered',
+ manual: 'manual'
+};
+
+function dayLabel(epoch) {
+ const d = new Date(epoch * 1000);
+ const today = new Date();
+ const yesterday = new Date(today.getTime() - 86400000);
+ const same = (a, b) => a.toDateString() === b.toDateString();
+ if (same(d, today)) return 'Today';
+ if (same(d, yesterday)) return 'Yesterday';
+ return d.toLocaleDateString(undefined, { weekday: 'short', month: 'short', day: 'numeric' });
+}
+
+function clockLabel(epoch) {
+ const d = new Date(epoch * 1000);
+ return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
+}
+
+function eventDetail(e) {
+ const v = e.values || {};
+ const bits = [];
+ if (v.battery_soc !== undefined) bits.push('battery ' + v.battery_soc + '%');
+ if (v.pv_total !== undefined) bits.push('solar ' + v.pv_total + 'W');
+ if (v.output_power_total !== undefined) bits.push('load ' + v.output_power_total + 'W');
+ if (v.ac_input_power) bits.push('grid ' + v.ac_input_power + 'W');
+ if (!bits.length) return '';
+ const who = e.source ? esc(e.source) + ': ' : '';
+ return who + bits.join(', ');
+}
+
+function renderEvents(events, multi) {
+ const box = document.getElementById('events');
+
+ if (!events || !events.length) {
+ box.innerHTML = '
Every change to a plug will appear here '
+ + 'with the reason for it.
';
+ return;
+ }
+
+ let html = '';
+ let lastDay = null;
+
+ events.forEach(e => {
+ const day = dayLabel(e.epoch);
+ if (day !== lastDay) {
+ html += '
' + day + '
';
+ lastDay = day;
+ }
+
+ const cause = e.cause || 'rule';
+ const label = CAUSE_LABEL[cause] || cause;
+ const why = e.rule ? esc(e.rule) : esc(e.reason || 'changed');
+ const detail = eventDetail(e);
+ const tip = [e.condition, detail].filter(Boolean).join(' | ');
+
+ html += '
'
+ + '
' + clockLabel(e.epoch) + '
'
+ + '
'
+ + (e.state ? 'ON' : 'OFF') + '
'
+ + '
' + esc(e.target || 'switch') + '
'
+ + '
'
+ + '' + label + ''
+ + why
+ + '
'
+ + (detail ? '
' + detail + '
' : '')
+ + '
';
+ });
+
+ box.innerHTML = html;
+}
+
function drawChart(history, devices) {
const canvas = document.getElementById('chart');
const ratio = window.devicePixelRatio || 1;
@@ -928,6 +1135,7 @@ async function refresh() {
document.getElementById('dot').className = data.engine ? 'dot' : 'dot stale';
renderAnker(data.anker);
renderShelly(data.shelly);
+ renderEvents(data.events, data.multi);
drawChart(data.history, data.anker);
} catch (err) {
failures++;