signalk-czone 0.3.0-beta.19 → 0.3.0-beta.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/README.md +6 -2
- package/index.js +12 -3
- package/lib/power-history.js +66 -0
- package/package.json +2 -2
- package/public/index.html +58 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0-beta.21
|
|
4
|
+
|
|
5
|
+
- Includes ZCF Favorites in the circuit favorites list, counts and stars by default. Browser additions and removals persist as local overrides across reloads and ZCF updates.
|
|
6
|
+
|
|
7
|
+
## 0.3.0-beta.20
|
|
8
|
+
|
|
9
|
+
- Shows individual monitoring readings as live for three minutes, then keeps their last values dimmed with their age; distinguishes stale rows from no data (#38, thanks @superstar76).
|
|
10
|
+
- Reconstructs earlier DC power trends from recorded voltage and current while preserving directly recorded power history.
|
|
11
|
+
|
|
3
12
|
## 0.3.0-beta.19
|
|
4
13
|
|
|
5
14
|
- Adds opt-in Monitoring for meters, tanks, temperatures, humidity, pressure and switch inputs, with expanded trends and circuit temperature chips (#36, thanks @superstar76).
|
package/README.md
CHANGED
|
@@ -202,7 +202,7 @@ Off by default: tick **Show the Monitoring tab** in the configuration panel to t
|
|
|
202
202
|
| Environment | pressure senders | 130314 |
|
|
203
203
|
| Inputs | switch inputs | (see Switch inputs) |
|
|
204
204
|
|
|
205
|
-
A reading is matched by the NMEA 2000 instance set in the Configuration Tool, and the temperature or pressure source or tank fluid type. Each
|
|
205
|
+
A reading is matched by the NMEA 2000 instance set in the Configuration Tool, and the temperature or pressure source or tank fluid type. Each reading is **LIVE** while it has updated in the last 3 minutes. After that its last value stays, dimmed, with its age ("Charge · 5 min ago"), counting up; a row with nothing live says when it was last updated. A row that has never had a reading says **No data received** with its instance ("No data received · instance 3"), which is the number to set on the sending device. NMEA 2000 "not available" values are never shown as readings.
|
|
206
206
|
|
|
207
207
|
**Which device.** Instances are not unique across devices: a Victron charger sends a battery status on the battery's instance as well as the shunt, and a wired meter can share an instance with another device. So each reading comes from one device:
|
|
208
208
|
|
|
@@ -211,7 +211,7 @@ A reading is matched by the NMEA 2000 instance set in the Configuration Tool, an
|
|
|
211
211
|
3. A third-party DC meter: where devices say what they are (127506), only one of the meter's DC type is used, and for a battery one that reports a state of charge beats one that does not, so a shunt beats a charger. Ties go to the lowest address, so the choice is the same after a restart.
|
|
212
212
|
4. Otherwise the first device heard keeps the reading until it has been quiet for two minutes.
|
|
213
213
|
|
|
214
|
-
Values are published under `electrical.czone.monitoring.<name>.*`, as the meters already were: `voltage`, `current`, `power`, `capacity.stateOfCharge`, `frequency`, `temperature`, `relativeHumidity`, `pressure`, `currentLevel`, `currentVolume`, `capacity`. **DC power** (watts) is volts × amps of the same 127508 message, so solar, alternator and wind output can be seen and trended.
|
|
214
|
+
Values are published under `electrical.czone.monitoring.<name>.*`, as the meters already were: `voltage`, `current`, `power`, `capacity.stateOfCharge`, `frequency`, `temperature`, `relativeHumidity`, `pressure`, `currentLevel`, `currentVolume`, `capacity`. **DC power** (watts) is volts × amps of the same 127508 message, so solar, alternator and wind output can be seen and trended. Its trend goes back as far as the meter's volts and amps: before power was first recorded, the chart works it out from them (the latest of each at that time; for long periods, from their averages, with the band from their ranges). Where power itself was recorded, those exact figures are used.
|
|
215
215
|
|
|
216
216
|
**Senders bridged onto NMEA 2000 by the same machine.** Signal K does not pass a plugin its own transmissions, or a GX's:
|
|
217
217
|
|
|
@@ -548,3 +548,7 @@ Certificate installation and app installation menus vary by device and browser.
|
|
|
548
548
|
|
|
549
549
|
The app still needs a live Signal K connection. No offline caching or service
|
|
550
550
|
worker is installed, and installing it does not grant circuit-write permission.
|
|
551
|
+
|
|
552
|
+
### Circuit favorites
|
|
553
|
+
|
|
554
|
+
Circuits marked Favorites in the loaded ZCF appear as favorites by default. The star buttons customize favorites for the current browser: you can add other circuits or hide a ZCF favorite. These choices survive browser and server restarts and apply when the ZCF is reloaded or updated. They are stored in browser site data, do not change the ZCF, and are independent on each browser/device. Clearing site data restores the ZCF defaults.
|
package/index.js
CHANGED
|
@@ -10,6 +10,7 @@ const monitoring = require('./lib/monitoring')
|
|
|
10
10
|
const sensors = require('./lib/sensors')
|
|
11
11
|
const { createCandump, PGNS: CANDUMP_PGNS } = require('./lib/candump')
|
|
12
12
|
const { createSourcePicker } = require('./lib/sources')
|
|
13
|
+
const { powerHistory } = require('./lib/power-history')
|
|
13
14
|
const { createTrends } = require('./lib/trends')
|
|
14
15
|
const signalk = require('./lib/signalk')
|
|
15
16
|
const confirmOffLib = require('./lib/confirm-off')
|
|
@@ -24,7 +25,9 @@ const CZONE_CONFIG_BLOCK_SIZE = 200
|
|
|
24
25
|
const CZONE_CONFIG_READ_TIMEOUT_MS = 45000
|
|
25
26
|
const PLUGIN_ID = 'signalk-czone'
|
|
26
27
|
// A monitored value older than this is no longer live (NOT ON BUS).
|
|
27
|
-
|
|
28
|
+
// A reading is LIVE while it has updated within this long; after that it is
|
|
29
|
+
// shown dimmed with its age ("Updated 5 min ago").
|
|
30
|
+
const MONITOR_LIVE_MS = 180e3
|
|
28
31
|
// A circuit's temperature is dropped after this long without a reading.
|
|
29
32
|
const TEMPERATURE_STALE_MS = 300e3
|
|
30
33
|
const MONITOR_GROUPS = { battery: 'Batteries', solar: 'Solar', alternator: 'Alternators', converter: 'Converters', wind: 'Wind Generators' }
|
|
@@ -1027,7 +1030,7 @@ module.exports = function (app) {
|
|
|
1027
1030
|
const now = Date.now()
|
|
1028
1031
|
const reading = (path, field) => {
|
|
1029
1032
|
const v = monitorValues.get(path)
|
|
1030
|
-
return { field, path, value: v ? v.value : null, at: v ? new Date(v.at).toISOString() : null, live: !!v && now - v.at < MONITOR_LIVE_MS }
|
|
1033
|
+
return { field, path, value: v ? v.value : null, at: v ? new Date(v.at).toISOString() : null, ageMs: v ? now - v.at : null, live: !!v && now - v.at < MONITOR_LIVE_MS }
|
|
1031
1034
|
}
|
|
1032
1035
|
const item = (group, kind, x, fields) => {
|
|
1033
1036
|
const readings = fields.filter(f => x.signalK[f]).map(f => reading(x.signalK[f], f))
|
|
@@ -1727,7 +1730,13 @@ module.exports = function (app) {
|
|
|
1727
1730
|
if (!trends) return res.json({ available: false, reason: 'not_started', data: [] })
|
|
1728
1731
|
const q = req.query
|
|
1729
1732
|
const custom = q.from !== undefined && q.to !== undefined
|
|
1730
|
-
|
|
1733
|
+
const range = custom ? { from: Number(q.from), to: Number(q.to) } : q.range
|
|
1734
|
+
// A DC meter's power: before it was first recorded, worked out from
|
|
1735
|
+
// the meter's recorded volts and amps (lib/power-history).
|
|
1736
|
+
const m = /^electrical\.czone\.monitoring\.(.+)\.power$/.exec(skPath)
|
|
1737
|
+
const meter = m && monitoringRuntime ? monitoringRuntime.meters.find(x => x.slug === m[1] && x.type === 'DC') : null
|
|
1738
|
+
if (meter) return res.json(powerHistory(trends.read(skPath, range), trends.read(meter.signalK.voltage, range), trends.read(meter.signalK.current, range)))
|
|
1739
|
+
res.json(trends.read(skPath, range))
|
|
1731
1740
|
})
|
|
1732
1741
|
|
|
1733
1742
|
router.get('/trend/status', (_req, res) => {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// DC power history for a chart.
|
|
4
|
+
//
|
|
5
|
+
// Live DC power is volts x amps of one 127508 message (lib/monitoring), and
|
|
6
|
+
// it is recorded like any other value. Before it was recorded (it is new in
|
|
7
|
+
// 0.3.0-beta.19) the meter's volts and amps were already being recorded, so
|
|
8
|
+
// the chart works that earlier part out from them:
|
|
9
|
+
// - each point is volts x amps, the latest of each at that time, as long as
|
|
10
|
+
// neither is older than the gap the chart allows;
|
|
11
|
+
// - for summarised periods (rows of [t, average, min, max]) the value is
|
|
12
|
+
// the product of the averages and the band is the range the two bands
|
|
13
|
+
// allow, so it is close but not exact.
|
|
14
|
+
// Wherever power itself was recorded, those exact figures are used.
|
|
15
|
+
//
|
|
16
|
+
// Inputs and output are the trend store's read() results:
|
|
17
|
+
// { available, data: [[t, value]] or [[t, avg, min, max]], gapMs, ... }
|
|
18
|
+
|
|
19
|
+
const round1 = v => Math.round(v * 10) / 10
|
|
20
|
+
const low = row => (row.length > 2 && row[2] !== null ? row[2] : row[1])
|
|
21
|
+
const high = row => (row.length > 3 && row[3] !== null ? row[3] : row[1])
|
|
22
|
+
|
|
23
|
+
// volts x amps, for times before `until` (ms; Infinity = all).
|
|
24
|
+
function multiply (volts, amps, until = Infinity) {
|
|
25
|
+
const v = (volts && volts.data) || []
|
|
26
|
+
const a = (amps && amps.data) || []
|
|
27
|
+
const gap = Math.max(volts.gapMs || 0, amps.gapMs || 0) || 16 * 60e3
|
|
28
|
+
const times = [...new Set([...v.map(r => r[0]), ...a.map(r => r[0])])].filter(t => t < until).sort((x, y) => x - y)
|
|
29
|
+
const out = []
|
|
30
|
+
let i = 0
|
|
31
|
+
let j = 0
|
|
32
|
+
let rv = null
|
|
33
|
+
let ra = null
|
|
34
|
+
for (const t of times) {
|
|
35
|
+
while (i < v.length && v[i][0] <= t) rv = v[i++]
|
|
36
|
+
while (j < a.length && a[j][0] <= t) ra = a[j++]
|
|
37
|
+
if (!rv || !ra || t - rv[0] > gap || t - ra[0] > gap) continue
|
|
38
|
+
if (rv.length > 2 || ra.length > 2) {
|
|
39
|
+
const corners = [low(rv) * low(ra), low(rv) * high(ra), high(rv) * low(ra), high(rv) * high(ra)]
|
|
40
|
+
out.push([t, round1(rv[1] * ra[1]), round1(Math.min(...corners)), round1(Math.max(...corners))])
|
|
41
|
+
} else {
|
|
42
|
+
out.push([t, round1(rv[1] * ra[1])])
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// power, volts, amps: read() results for the same range.
|
|
49
|
+
function powerHistory (power, volts, amps) {
|
|
50
|
+
if (!power || !power.available || power.error) return power
|
|
51
|
+
if (!volts || !volts.available || !amps || !amps.available) return power
|
|
52
|
+
const recorded = Array.isArray(power.data) ? power.data : []
|
|
53
|
+
const first = recorded.length ? recorded[0][0] : Infinity
|
|
54
|
+
const before = multiply(volts, amps, first)
|
|
55
|
+
if (!before.length) return power
|
|
56
|
+
return {
|
|
57
|
+
...power,
|
|
58
|
+
data: before.concat(recorded),
|
|
59
|
+
points: before.length + recorded.length,
|
|
60
|
+
gapMs: Math.max(power.gapMs || 0, volts.gapMs || 0, amps.gapMs || 0) || undefined,
|
|
61
|
+
// Up to here the figures are worked out from volts x amps.
|
|
62
|
+
computedUntil: before[before.length - 1][0]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { powerHistory, multiply }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "signalk-czone",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.21",
|
|
4
4
|
"description": "BETA: Signal K CZone monitoring and control using dynamically parsed ZCF configuration and NMEA 2000",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"keywords": [
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
"icon.png"
|
|
43
43
|
],
|
|
44
44
|
"scripts": {
|
|
45
|
-
"test": "node test/zcf.test.js && node test/zcf-fixtures.test.js && node test/circuit-state.test.js && node test/czone.test.js && node test/nmea2000.test.js && node test/control-x-plus.test.js && node test/signalk.test.js && node test/write.test.js && node test/circuit-status.test.js && node test/status-mapping-regression.test.js && node test/state.test.js && node test/config-network.test.js && node test/config-namespace.test.js && node test/monitoring.test.js && node test/sensors.test.js && node test/monitoring-plugin.test.js && node test/confirm-off.test.js && node test/webapp-current.test.js && node test/webapp-auth.test.js && node test/webapp-monitoring.test.js && node test/bridge.test.js && node test/victron-switches.test.js && node test/victron-label-steady.test.js && node test/switch-pane-plugin.test.js && node test/trends.test.js && node test/inputs.test.js && node --require ./test/helpers/gx-low-space.cjs test/trends.test.js"
|
|
45
|
+
"test": "node test/zcf.test.js && node test/zcf-fixtures.test.js && node test/circuit-state.test.js && node test/czone.test.js && node test/nmea2000.test.js && node test/control-x-plus.test.js && node test/signalk.test.js && node test/write.test.js && node test/circuit-status.test.js && node test/status-mapping-regression.test.js && node test/state.test.js && node test/config-network.test.js && node test/config-namespace.test.js && node test/monitoring.test.js && node test/sensors.test.js && node test/monitoring-plugin.test.js && node test/power-history.test.js && node test/confirm-off.test.js && node test/webapp-current.test.js && node test/webapp-favorites.test.js && node test/webapp-auth.test.js && node test/webapp-monitoring.test.js && node test/bridge.test.js && node test/victron-switches.test.js && node test/victron-label-steady.test.js && node test/switch-pane-plugin.test.js && node test/trends.test.js && node test/inputs.test.js && node --require ./test/helpers/gx-low-space.cjs test/trends.test.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
48
|
"signalk-czone-zcf": "0.1.0-beta.2"
|
package/public/index.html
CHANGED
|
@@ -118,6 +118,9 @@
|
|
|
118
118
|
.mon-val.on { border-color:rgba(46,213,115,.55); background:rgba(46,213,115,.12); }
|
|
119
119
|
.mon-live { margin-left:10px; font-size:10px; font-weight:700; letter-spacing:.6px; color:#2ed573; white-space:nowrap; }
|
|
120
120
|
.mon-row.off .mon-live { color:#ffb84d; }
|
|
121
|
+
/* A reading that has not updated for 3 minutes: dimmed, with its age. */
|
|
122
|
+
.mon-val.stale { opacity:.55; }
|
|
123
|
+
.mon-val.stale small { color:#ffb84d; }
|
|
121
124
|
.temp-chip { display:inline-block; margin-left:8px; padding:1px 7px; border-radius:9px; border:1px solid rgba(24,213,232,.35); background:rgba(24,213,232,.08); color:var(--cyan); font:inherit; font-size:11px; font-weight:600; vertical-align:1px; }
|
|
122
125
|
button.temp-chip { cursor:pointer; }
|
|
123
126
|
@media (max-width: 600px) {
|
|
@@ -500,6 +503,10 @@
|
|
|
500
503
|
const favoritesKey = 'signalk-czone-circuits:favorites';
|
|
501
504
|
let favorites = new Set(JSON.parse(localStorage.getItem(favoritesKey) || '[]'));
|
|
502
505
|
|
|
506
|
+
// ZCF favorites are defaults; browser choices can add or hide them locally.
|
|
507
|
+
const hiddenFavoritesKey = `${favoritesKey}:hidden`;
|
|
508
|
+
let hiddenFavorites = new Set(JSON.parse(localStorage.getItem(hiddenFavoritesKey) || '[]'));
|
|
509
|
+
|
|
503
510
|
const esc = s => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
504
511
|
const nameKey = c => c.slug || c.name;
|
|
505
512
|
const stateOf = c => c.state && c.state.state === 'ON';
|
|
@@ -510,6 +517,7 @@
|
|
|
510
517
|
return current != null && current > 0 ? `${current.toFixed(1)} A` : null;
|
|
511
518
|
};
|
|
512
519
|
const categoryData = c => c.categories || { master: [], sub: [], userDefined: [] };
|
|
520
|
+
const isFavorite = c => !hiddenFavorites.has(nameKey(c)) && (favorites.has(nameKey(c)) || (categoryData(c).master || []).includes('Favorites'));
|
|
513
521
|
const allCategories = c => [...(categoryData(c).master || []), ...(categoryData(c).sub || []), ...(categoryData(c).userDefined || [])];
|
|
514
522
|
const catList = () => {
|
|
515
523
|
const assigned = new Set(circuits.flatMap(allCategories).filter(c => !topLevelCategories.has(c)));
|
|
@@ -524,14 +532,17 @@
|
|
|
524
532
|
const icon = c => MFD ? mfdIcon(c) : (categoryIcons[c] || '•');
|
|
525
533
|
const color = c => categoryColors[c] || '#4bb5ff';
|
|
526
534
|
|
|
527
|
-
function saveFavorites() {
|
|
535
|
+
function saveFavorites() {
|
|
536
|
+
localStorage.setItem(favoritesKey, JSON.stringify([...favorites]));
|
|
537
|
+
localStorage.setItem(hiddenFavoritesKey, JSON.stringify([...hiddenFavorites]));
|
|
538
|
+
}
|
|
528
539
|
function setView(v) { view=v; category=null; render(); if (v === 'monitor') { monitorLastFetch = 0; refreshMonitoring(); } }
|
|
529
540
|
function setCategory(c) { category=c; view='category'; render(); }
|
|
530
541
|
window.setView = setView; window.setCategory = setCategory;
|
|
531
542
|
|
|
532
543
|
function renderNav() {
|
|
533
544
|
const items = [
|
|
534
|
-
['all','⌂','All Circuits',circuits.length], ['inuse','●','In Use',activeCount()], ['favorites','☆','Favorites',circuits.filter(
|
|
545
|
+
['all','⌂','All Circuits',circuits.length], ['inuse','●','In Use',activeCount()], ['favorites','☆','Favorites',circuits.filter(isFavorite).length],
|
|
535
546
|
['ac','♆','AC',masterCount('AC')], ['dc','▣','DC',masterCount('DC')]
|
|
536
547
|
];
|
|
537
548
|
if (monitor.items.length) items.push(['monitor','◔','Monitoring',monitor.items.length]);
|
|
@@ -549,7 +560,7 @@
|
|
|
549
560
|
let list = circuits.filter(c => {
|
|
550
561
|
if (c.showInCircuitList === false) return false;
|
|
551
562
|
if (view==='inuse' && !stateOf(c)) return false;
|
|
552
|
-
if (view==='favorites' && !
|
|
563
|
+
if (view==='favorites' && !isFavorite(c)) return false;
|
|
553
564
|
if (view==='ac' && !(categoryData(c).master || []).includes('AC')) return false;
|
|
554
565
|
if (view==='dc' && !(categoryData(c).master || []).includes('DC')) return false;
|
|
555
566
|
if (category && !allCategories(c).includes(category)) return false;
|
|
@@ -589,12 +600,26 @@
|
|
|
589
600
|
showTrend({ path, title: decodeURIComponent(name), r: readingFor(item, field) });
|
|
590
601
|
};
|
|
591
602
|
|
|
603
|
+
// How long ago a reading arrived: its age when fetched (by the server's
|
|
604
|
+
// clock, so a tablet or plotter with the wrong time does not matter), plus
|
|
605
|
+
// the time since. It counts up as the tab refreshes.
|
|
606
|
+
function monitorAge(ageMs) {
|
|
607
|
+
const ms = ageMs + (monitor.fetchedAt ? Date.now() - monitor.fetchedAt : 0);
|
|
608
|
+
const min = Math.floor(ms / 60000);
|
|
609
|
+
if (min < 60) return `${Math.max(1, min)} min ago`;
|
|
610
|
+
const h = Math.floor(min / 60);
|
|
611
|
+
if (h < 24) return `${h} h ${min % 60} min ago`;
|
|
612
|
+
return `${Math.floor(h / 24)} d ago`;
|
|
613
|
+
}
|
|
614
|
+
|
|
592
615
|
function monitorValue(item, r) {
|
|
593
616
|
const u = readingFor(item, r.field);
|
|
594
|
-
const
|
|
617
|
+
const stale = r.live === false && Number.isFinite(r.ageMs);
|
|
618
|
+
const box = `<b>${u.f(r.value).toFixed(u.dp)}</b><i>${u.unit}</i><small>${stale ? `${u.label} · ${monitorAge(r.ageMs)}` : u.label}</small>`;
|
|
619
|
+
const cls = `mon-val${stale ? ' stale' : ''}`;
|
|
595
620
|
return monitor.trendsEnabled && r.field !== 'capacity'
|
|
596
|
-
? `<button type="button" class="
|
|
597
|
-
: `<div class="
|
|
621
|
+
? `<button type="button" class="${cls}" title="${u.label} trend" onclick="openSeries('${encodeURIComponent(r.path)}','${encodeURIComponent(item.name)}','${r.field}')">${box}</button>`
|
|
622
|
+
: `<div class="${cls}">${box}</div>`;
|
|
598
623
|
}
|
|
599
624
|
|
|
600
625
|
function monitorRow(item) {
|
|
@@ -603,11 +628,18 @@
|
|
|
603
628
|
values = item.state === null ? '' : `<div class="mon-val ${item.state ? 'on' : ''}"><b>${item.state ? 'ON' : 'OFF'}</b><small>Switch input</small></div>`;
|
|
604
629
|
sub = item.decoded === false ? 'This kind of module is not read yet' : item.live ? (item.since ? `Since ${new Date(item.since).toLocaleTimeString([], { hour:'2-digit', minute:'2-digit' })}` : 'Switch input') : 'No status from its module yet';
|
|
605
630
|
} else {
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
631
|
+
// LIVE while a reading updated in the last 3 minutes; older readings
|
|
632
|
+
// dimmed with their age; "No data received" when nothing has arrived.
|
|
633
|
+
values = item.readings.map(r => monitorValue(item, r)).join('');
|
|
634
|
+
if (item.readings.length && item.capacity) values += `<div class="mon-val${item.live ? '' : ' stale'}"><b>${Math.round(item.capacity * 1000)}</b><i>L</i><small>Capacity</small></div>`;
|
|
635
|
+
const ages = item.readings.map(r => r.ageMs).filter(Number.isFinite);
|
|
636
|
+
const where = `instance ${item.instance}${item.thirdParty ? ' · third-party' : ''}`;
|
|
637
|
+
sub = item.live ? `Instance ${item.instance}${item.thirdParty ? ' · third-party' : ''}`
|
|
638
|
+
: ages.length ? `Updated ${monitorAge(Math.min.apply(null, ages))} · ${where}`
|
|
639
|
+
: `No data received · ${where}`;
|
|
609
640
|
}
|
|
610
|
-
|
|
641
|
+
const badge = item.live ? 'LIVE' : (item.kind !== 'input' && item.readings.length ? 'NOT LIVE' : 'NO DATA');
|
|
642
|
+
return `<div class="mon-row ${item.live ? '' : 'off'}"><div class="circuit-name"><strong>${esc(item.name)}</strong><small>${esc(sub)}</small></div><div class="mon-values">${values}</div><span class="mon-live">${badge}</span></div>`;
|
|
611
643
|
}
|
|
612
644
|
|
|
613
645
|
function renderMonitoring() {
|
|
@@ -637,7 +669,7 @@
|
|
|
637
669
|
const data = await r.json();
|
|
638
670
|
const chips = () => JSON.stringify(Object.keys(monitor.temperatures).map(k => [k, monitor.temperatures[k].kelvin.toFixed(1)]).concat([monitor.items.length, monitor.trendsEnabled]));
|
|
639
671
|
const before = chips();
|
|
640
|
-
monitor = { items: Array.isArray(data.items) ? data.items : [], temperatures: data.temperatures || {}, trendsEnabled: data.trendsEnabled === true };
|
|
672
|
+
monitor = { items: Array.isArray(data.items) ? data.items : [], temperatures: data.temperatures || {}, trendsEnabled: data.trendsEnabled === true, fetchedAt: Date.now() };
|
|
641
673
|
if (view === 'monitor' && !category) { renderMonitoring(); renderNav(); }
|
|
642
674
|
else if (chips() !== before) render(); // only when a chip or the tab changed: a redraw would cut short a slider being moved
|
|
643
675
|
} catch (_) { /* the next round tries again */ }
|
|
@@ -652,7 +684,7 @@
|
|
|
652
684
|
}
|
|
653
685
|
|
|
654
686
|
function circuitRow(c) {
|
|
655
|
-
const on = stateOf(c), pct = percentOf(c), current = currentLabel(c), dim = !!(c.capabilities && c.capabilities.dimmer), fav =
|
|
687
|
+
const on = stateOf(c), pct = percentOf(c), current = currentLabel(c), dim = !!(c.capabilities && c.capabilities.dimmer), fav = isFavorite(c);
|
|
656
688
|
const p = pending.get(nameKey(c));
|
|
657
689
|
const cats = subCategoryList(c).slice(0,3).map(x => `<span class="tag cat" role="img" tabindex="0" style="--cat:${color(x)}" title="${esc(x)}" aria-label="${esc(x)} category">${icon(x)}</span>`).join('');
|
|
658
690
|
const disabled = p && p.kind === 'state';
|
|
@@ -1060,7 +1092,19 @@
|
|
|
1060
1092
|
}
|
|
1061
1093
|
putSignalK(signalKPath(c, 'brightness'), requested / 100, c, 'brightness', requested, previous);
|
|
1062
1094
|
};
|
|
1063
|
-
window.toggleFavorite = key => {
|
|
1095
|
+
window.toggleFavorite = key => {
|
|
1096
|
+
const circuit = circuits.find(c => nameKey(c) === key);
|
|
1097
|
+
if (!circuit) return;
|
|
1098
|
+
if (isFavorite(circuit)) {
|
|
1099
|
+
favorites.delete(key);
|
|
1100
|
+
hiddenFavorites.add(key);
|
|
1101
|
+
} else {
|
|
1102
|
+
hiddenFavorites.delete(key);
|
|
1103
|
+
favorites.add(key);
|
|
1104
|
+
}
|
|
1105
|
+
saveFavorites();
|
|
1106
|
+
render();
|
|
1107
|
+
};
|
|
1064
1108
|
|
|
1065
1109
|
// ---- Trends ---------------------------------------------------------------
|
|
1066
1110
|
// The arrow at the end of a row opens that circuit's current over time, at
|
|
@@ -1375,7 +1419,7 @@
|
|
|
1375
1419
|
}
|
|
1376
1420
|
|
|
1377
1421
|
function renderStats() {
|
|
1378
|
-
const on=activeCount(), total=circuits.length, fav=circuits.filter(
|
|
1422
|
+
const on=activeCount(), total=circuits.length, fav=circuits.filter(isFavorite).length;
|
|
1379
1423
|
document.querySelector('#stats').innerHTML = `<div class="stat"><div class="stat-icon">⚡</div><div><strong>${total}</strong><span>Total Circuits</span></div></div><div class="stat"><div class="stat-icon">●</div><div><strong>${on}</strong><span>In Use</span></div></div><div class="stat"><div class="stat-icon">○</div><div><strong>${total-on}</strong><span>Off</span></div></div><div class="stat"><div class="stat-icon">★</div><div><strong>${fav}</strong><span>Favorites</span></div></div>`;
|
|
1380
1424
|
const n2kReady = status.nmeaReady === true;
|
|
1381
1425
|
const readyColor = n2kReady ? '#64e4aa' : '#ffcc77';
|