Files
2026-06-18 20:14:37 +00:00

1788 lines
57 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// LIVE DATA — PHP (step 1 of 3)
// Reads the same session as index.php so we know which automation server (asp)
// is selected. That server's numeric id becomes keyid for getData requests.
//
// Workflow: log in on index.php → pick office / building / asp → open test.php
include("common.php");
include("ingest.php");
include("session.php");
session_start();
$basKeyid = null; // numeric id sent as keyid= in getData (NOT the name "asp")
$basKeyname = ""; // human label e.g. "asp" — shown in header only
$basLiveHint = "Log in on index.php, select office / building / asp, then open this page.";
$Client = new Session();
$BasData = new Ingest();
if ($Client->auth()) {
// Rebuild cust / site / key indexes from session — same logic as index.php sidebar
$custIndex = isset($_SESSION["custIndex"]) ? $_SESSION["custIndex"] : 0;
$custList = $Client->getCustList($_SESSION["office"]);
$siteIndex = isset($_SESSION["siteIndex"]) ? $_SESSION["siteIndex"] : 0;
$siteList = $Client->getSites($custList[$custIndex]["id"]);
$keyIndex = isset($_SESSION["keyIndex"]) ? $_SESSION["keyIndex"] : 0;
$keyList = $BasData->getKeys($siteList[$siteIndex]["id"]);
if (count($keyList) > 0 && isset($keyList[$keyIndex])) {
$basKeyid = $keyList[$keyIndex]["id"]; // ← this is what index.php puts in #sitekeys value
$basKeyname = $keyList[$keyIndex]["keyname"]; // ← this is what you see in the dropdown ("asp")
$basLiveHint = "";
}
}
// Optional override: test.php?keyid=3 (same number as asp dropdown value on index)
if (isset($_GET["keyid"]) && $_GET["keyid"] !== "") {
$basKeyid = (int)$_GET["keyid"];
}
// Force sample data: test.php?sample=1 (ignores login — auto-loads rtu-sample.js + veris-sample.js)
$basForceSample = isset($_GET["sample"]) && $_GET["sample"] !== "" && $_GET["sample"] !== "0";
if ($basForceSample) {
$basKeyid = null;
$basLiveHint = "Sample data mode — rtu-sample.js and veris-sample.js (matches forecast.py).";
}
// Force forecast overlay: test.php?forecast=1 (auto-loads forecast_output.json)
$basForceForecast = isset($_GET["forecast"]) && $_GET["forecast"] !== "" && $_GET["forecast"] !== "0";
// reports.php iframe: test.php?embed=1 hides duplicate page title
$basEmbed = isset($_GET["embed"]) && $_GET["embed"] !== "" && $_GET["embed"] !== "0";
// Optional: test.php?interval=48&tstamp=NOW (or YYYY-MM-DD) — same as reports sidebar
$basInterval = 35;
if (isset($_GET["interval"]) && $_GET["interval"] !== "") {
$basInterval = min(max(8, (int)$_GET["interval"]), 72);
}
$basTstamp = "NOW";
if (isset($_GET["tstamp"]) && $_GET["tstamp"] !== "") {
$basTstamp = $_GET["tstamp"];
}
?>
<!DOCTYPE html>
<html lang="en" data-bs-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>BAS Data</title>
<link href="css/bootstrap.min.css" rel="stylesheet" />
<link href="css/styles.css" rel="stylesheet" />
<link rel="icon" href="/favicon.png">
</head>
<body>
<div id="container">
<!-- DASHBOARD HEADER -->
<div class="mb-2 px-1">
<?php if (!$basEmbed) { ?>
<h4 class="mb-0 text-light">Device Snapshot</h4>
<?php // LIVE DATA — shows which key PHP found (or instructions if not logged in) ?>
<?php if ($basLiveHint !== "") { ?>
<div class="small text-muted mt-1"><?php echo htmlspecialchars($basLiveHint); ?></div>
<?php } else { ?>
<div class="small text-muted mt-1">Live key: <b><?php echo htmlspecialchars($basKeyname); ?></b> (keyid <?php echo (int)$basKeyid; ?>)</div>
<?php } ?>
<?php } ?>
<div class="d-flex flex-wrap gap-2 align-items-start mt-2">
<div id="basLiveStatusBar" class="alert py-2 px-3 mb-0 small d-none flex-grow-1" role="status">
<div id="basLiveStatusText"></div>
<div id="basLiveStatusProgress" class="progress mt-2 d-none" style="height: 4px">
<div class="progress-bar progress-bar-striped progress-bar-animated w-100"></div>
</div>
</div>
<?php if (!$basForceSample) { ?>
<button type="button" class="btn btn-sm btn-outline-info" id="basLoadSampleBtn">Load sample data</button>
<?php } ?>
<?php if (!$basForceForecast) { ?>
<button type="button" class="btn btn-sm btn-outline-warning" id="basLoadForecastBtn">Load forecast</button>
<?php } ?>
</div>
</div>
<!-- TOOLBAR - checkbox data-chart="X" must match chartBox-X below (hidden in reports.php iframe) -->
<?php if (!$basEmbed) { ?>
<div id="chartToolbar" class="box shadow chart-box chart-toolbar-compact mb-2">
<div class="d-flex flex-wrap gap-1 align-items-center">
<button type="button" class="btn btn-sm btn-outline-secondary py-0" id="chartsHideAll">Hide all</button>
<button type="button" class="btn btn-sm btn-outline-secondary py-0" id="chartsShowAll">Show all</button>
<span class="text-muted mx-1 small">|</span>
<span class="text-muted me-1 small">Show:</span>
<div class="form-check form-check-inline form-check-sm mb-0">
<input class="form-check-input chart-load-toggle" type="checkbox" id="toggle-sparks" data-chart="sparks" checked>
<label class="form-check-label" for="toggle-sparks">Sparks</label>
</div>
<div class="form-check form-check-inline form-check-sm mb-0">
<input class="form-check-input chart-load-toggle" type="checkbox" id="toggle-heatmap" data-chart="heatmap" checked>
<label class="form-check-label" for="toggle-heatmap">Heatmap</label>
</div>
<div class="form-check form-check-inline form-check-sm mb-0">
<input class="form-check-input chart-load-toggle" type="checkbox" id="toggle-lineP" data-chart="lineP" checked>
<label class="form-check-label" for="toggle-lineP">P line</label>
</div>
<div class="form-check form-check-inline form-check-sm mb-0">
<input class="form-check-input chart-load-toggle" type="checkbox" id="toggle-phaseCompare" data-chart="phaseCompare" checked>
<label class="form-check-label" for="toggle-phaseCompare">Phase compare</label>
</div>
<div class="form-check form-check-inline form-check-sm mb-0">
<input class="form-check-input chart-load-toggle" type="checkbox" id="toggle-radialBar" data-chart="radialBar" checked>
<label class="form-check-label" for="toggle-radialBar">Temp vs setpoint</label>
</div>
<div class="form-check form-check-inline form-check-sm mb-0">
<input class="form-check-input chart-load-toggle" type="checkbox" id="toggle-hvacMode" data-chart="hvacMode" checked>
<label class="form-check-label" for="toggle-hvacMode">HVAC mode</label>
</div>
<div class="form-check form-check-inline form-check-sm mb-0">
<input class="form-check-input chart-load-toggle" type="checkbox" id="toggle-trigger" data-chart="trigger">
<label class="form-check-label" for="toggle-trigger">Trigger</label>
</div>
</div>
</div>
<?php } ?>
<!-- SPARK KPIs - "Sparks" checkbox hides chartBox-sparks; filled by basApplyDashboard() -->
<div class="mb-2" id="chartBox-sparks" data-chart="sparks">
<div id="sparkRow" class="row g-2">
<div class="col-md-4 col-sm-6">
<div class="spark-card box shadow">
<div class="spark-meta">
<span class="spark-label">Fan runtime</span>
<span class="spark-value" id="sparkFanValue">--</span> <!-- big % filled by JS -->
</div>
<div class="spark-sub" id="sparkFanSub">—</div>
<div id="sparkFan"></div> <!-- sparkline draws here -->
</div>
</div>
<div class="col-md-4 col-sm-6">
<div class="spark-card box shadow">
<div class="spark-meta">
<span class="spark-label">Power runtime</span>
<span class="spark-value" id="sparkPowerValue">--</span> <!-- big % filled by JS -->
</div>
<div class="spark-sub" id="sparkPowerSub">—</div>
<div id="sparkPower"></div> <!-- sparkline draws here -->
</div>
</div>
<div class="col-md-4 col-sm-12">
<div class="spark-card box shadow">
<div class="spark-meta">
<span class="spark-label">Room temp</span>
<span class="spark-value" id="sparkRoomValue">--</span> <!-- big °F filled by JS -->
</div>
<div class="spark-sub" id="sparkRoomSub">—</div>
<div id="sparkRoom"></div> <!-- sparkline draws here -->
</div>
</div>
</div>
</div>
<div id="basDashboardSummary" class="box shadow small py-2 px-3 mb-2 d-none">
<div class="d-flex flex-wrap gap-2 align-items-center text-muted">
<span id="basSummaryWindow"></span>
<span class="text-secondary">|</span>
<span id="basSummaryFresh"></span>
</div>
<div id="basSummaryStats" class="mt-1 text-light small"></div>
</div>
<!-- CHARTS - each chartBox-* wraps a div the chart draws into -->
<div class="box shadow chart-box" id="chartBox-heatmap" data-chart="heatmap">
<div id="chartCont"></div> <!-- Apex heatmap -->
</div>
<div class="box shadow chart-box" id="chartBox-lineP" data-chart="lineP">
<div id="lineChartCont"></div> <!-- Apex line: P -->
</div>
<div class="box shadow chart-box" id="chartBox-phaseCompare" data-chart="phaseCompare">
<div id="phaseCompareCont"></div> <!-- Apex line: Ia/Ib/Ic -->
</div>
<div class="box shadow chart-box" id="chartBox-radialBar" data-chart="radialBar">
<div id="radialBarCont"></div> <!-- Apex column: actual temp vs setpoint (0 = set) -->
</div>
<div class="box shadow chart-box" id="chartBox-hvacMode" data-chart="hvacMode">
<div id="hvacModeCont"></div> <!-- Apex rangeBar timeline: HVACMode -->
</div>
<div class="box shadow chart-box" id="chartBox-trigger" data-chart="trigger" style="display:none">
<div id="TriggerCont"></div> <!-- Apex bar: FanSts start count per day/hour -->
</div>
</div>
<script src="js/test.js"></script> <!-- colors + dark theme - loaded before charts -->
<script src="js/apexcharts.js"></script>
<?php if ($basForceSample) { ?>
<script src="js/veris-sample.js?v=<?php echo (int)@filemtime(__DIR__ . '/js/veris-sample.js'); ?>"></script>
<script src="js/rtu-sample.js?v=<?php echo (int)@filemtime(__DIR__ . '/js/rtu-sample.js'); ?>"></script>
<?php } ?>
<script>
// LIVE DATA — JS config (step 2 of 3)
// PHP above injects keyid into this object. JS uses it when calling index.php.
var BAS_FORCE_SAMPLE = <?php echo $basForceSample ? "true" : "false"; ?>;
var BAS_FORCE_FORECAST = <?php echo $basForceForecast ? "true" : "false"; ?>;
var BAS_LIVE = {
keyid: <?php echo $basKeyid !== null ? json_encode((string)$basKeyid) : "null"; ?>, // null = no live fetch unless you click Load sample data
keyname: <?php echo json_encode($basKeyname); ?>, // display only (e.g. "asp")
fanDevice: "RTU_3",
verisDevice: "VerisMeter",
interval: <?php echo (int)$basInterval; ?>, // hours of history — same as index getData(device) uses
tstamp: <?php echo json_encode($basTstamp); ?> // NOW or YYYY-MM-DD end anchor
};
</script>
<script>
// D3 drawHeatmap / drawSystemInfo live on index.php only — this page uses ApexCharts
// ----- APEX HEATMAP CONFIG (not using test.js colors - left standalone) -----
var optHeatMap = {
theme: {
mode: 'dark',
palette: 'palette3',
monochrome: {
enabled: false,
color: '#255aee',
shadeTo: 'dark',
shadeIntensity: 0.65
},
},
series: [],
chart: {
height: 350,
type: 'heatmap',
background: '#333335',
animations: {
enabled: false,
}
},
dataLabels: {
enabled: false
},
colors: ["#FEB019"],
title: {
text: 'All Points HeatMap'
},
grid: {
xaxis: {
lines: {
show: false
}
},
yaxis: {
lines: {
show: false
}
}
},
xaxis: {
type: 'datetime',
labels: {
datetimeUTC: false
}
},
plotOptions: {
heatmap: {
distributed: true,
useFillColorAsStroke: true,
radius: 0
}
}
};
// ----- SPARK KPIs (top row) -----
// fan + room read rtuData (rtu-sample.js), power reads verisData (veris-sample.js) //can be deleted just to show data
// LIVE DATA — fetch helper (step 3 of 3)
// Copied from index.php getData(device). POSTs to index.php; server returns JSON
// shaped like rtu-sample.js / veris-sample.js: { FanSts: [{x,y},...], ... }
function basGetData(keyid, deviceName, callback) {
var xhttp = new XMLHttpRequest();
xhttp.onload = function() {
try {
callback(null, JSON.parse(this.responseText));
} catch (e) {
callback(e || new Error("bad json"));
}
};
xhttp.onerror = function() {
callback(new Error("network"));
};
xhttp.open("POST", "index.php");
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
xhttp.send(
"getData=true&keyid=" + encodeURIComponent(keyid) + // which asp / automation server
"&name=" + encodeURIComponent(deviceName) + // which device e.g. RTU_3
"&tstamp=" + encodeURIComponent(BAS_LIVE.tstamp) +
"&interval=" + BAS_LIVE.interval
);
}
// LIVE DATA — header status bar (loading / live on / failed / sample only)
var basLiveFetchState = { rtu: "idle", veris: "idle" };
function basShowLiveStatus(kind, message) {
var bar = document.getElementById("basLiveStatusBar");
var text = document.getElementById("basLiveStatusText");
var prog = document.getElementById("basLiveStatusProgress");
if (!bar || !text) {
return;
}
var classes = {
sample: "alert-secondary",
loading: "alert-info",
ok: "alert-success",
warn: "alert-warning",
error: "alert-danger"
};
bar.className = "alert py-2 px-3 mt-2 mb-0 small " + (classes[kind] || "alert-secondary");
text.textContent = message;
if (prog) {
prog.classList.toggle("d-none", kind !== "loading");
}
bar.classList.remove("d-none");
}
function basRefreshLiveStatusBar() {
if (!BAS_LIVE.keyid) {
return;
}
if (basLiveFetchState.rtu === "loading" || basLiveFetchState.veris === "loading") {
return;
}
var failed = [];
var loaded = [];
if (basLiveFetchState.rtu === "fail") {
failed.push(BAS_LIVE.fanDevice);
} else if (basLiveFetchState.rtu === "ok") {
loaded.push(BAS_LIVE.fanDevice);
}
if (basLiveFetchState.veris === "fail") {
failed.push(BAS_LIVE.verisDevice);
} else if (basLiveFetchState.veris === "ok") {
loaded.push(BAS_LIVE.verisDevice);
}
if (failed.length === 0) {
basShowLiveStatus("ok", "Live data on — " + loaded.join(" + ") + ".");
} else if (loaded.length === 0) {
basShowLiveStatus("error", "Live data failed — click Load sample data for offline demo files.");
} else {
basShowLiveStatus("warn", "Live data partial — " + loaded.join(", ") + " ok; " + failed.join(", ") + " failed.");
}
}
function basInitLiveStatusBar() {
if (!BAS_LIVE.keyid) {
if (!BAS_FORCE_SAMPLE) {
basShowLiveStatus("sample", "No live key — log in on index.php or click Load sample data.");
}
return;
}
basLiveFetchState.rtu = "loading";
basLiveFetchState.veris = "loading";
basShowLiveStatus("loading", "Loading live data from " + BAS_LIVE.fanDevice + " and " + BAS_LIVE.verisDevice + "…");
}
var BAS_STALE_MS = 2 * 60 * 60 * 1000;
function basBundleTimeRange(rtuBundle, verisBundle) {
var minMs = null;
var maxMs = null;
function scan(bundle) {
var keys, k, arr, first, last;
if (!bundle) {
return;
}
keys = Object.keys(bundle);
for (k = 0; k < keys.length; k++) {
arr = bundle[keys[k]];
if (!arr || !arr.length) {
continue;
}
first = arr[0].x;
last = arr[arr.length - 1].x;
if (minMs === null || first < minMs) {
minMs = first;
}
if (maxMs === null || last > maxMs) {
maxMs = last;
}
}
}
scan(rtuBundle);
scan(verisBundle);
return { minMs: minMs, maxMs: maxMs };
}
function basHvacModePctSummary(bars) {
var byMode = {};
var total = 0;
var parts, name;
if (!bars || !bars.length) {
return "";
}
for (var i = 0; i < bars.length; i++) {
var dur = bars[i].y[1] - bars[i].y[0];
if (dur <= 0) {
continue;
}
total += dur;
byMode[bars[i].modeLabel || bars[i].x] = (byMode[bars[i].modeLabel || bars[i].x] || 0) + dur;
}
if (!total) {
return "";
}
parts = [];
for (name in byMode) {
if (Object.prototype.hasOwnProperty.call(byMode, name)) {
parts.push(name + " " + Math.round((byMode[name] / total) * 1000) / 10 + "%");
}
}
parts.sort();
return "HVAC " + parts.join(" · ");
}
function basUpdateDashboardSummary(rtuBundle, verisBundle, rtuResolved, verisResolved) {
var el = document.getElementById("basDashboardSummary");
var windowEl = document.getElementById("basSummaryWindow");
var freshEl = document.getElementById("basSummaryFresh");
var statsEl = document.getElementById("basSummaryStats");
var range, through, ageMs, stale, parts, fanSlot, powerSlot, roomSlot, hvacText;
if (!el || !windowEl || !freshEl || !statsEl) {
return;
}
range = basBundleTimeRange(rtuBundle, verisBundle);
if (range.maxMs === null) {
el.classList.add("d-none");
return;
}
el.classList.remove("d-none");
through = new Date(range.maxMs);
ageMs = Date.now() - range.maxMs;
stale = ageMs > BAS_STALE_MS;
windowEl.textContent = BAS_LIVE.interval + "h window";
freshEl.className = stale ? "text-warning" : "text-success";
freshEl.textContent = "Through " + through.toLocaleString();
if (stale) {
freshEl.textContent += " — last reading " + basFormatDuration(range.maxMs, Date.now()) + " ago";
}
parts = [];
fanSlot = rtuResolved.spark_fan;
if (fanSlot && rtuBundle[fanSlot.point] && rtuBundle[fanSlot.point].length) {
parts.push("Fan " + basOnPercent(rtuBundle[fanSlot.point], fanSlot.onThreshold != null ? fanSlot.onThreshold : 0.5) + "% on");
}
powerSlot = verisResolved.spark_power;
if (powerSlot && verisBundle[powerSlot.point] && verisBundle[powerSlot.point].length) {
parts.push("Power " + basOnPercent(verisBundle[powerSlot.point], powerSlot.onThreshold != null ? powerSlot.onThreshold : 0.5) + "% on");
}
roomSlot = rtuResolved.spark_room;
if (roomSlot && rtuBundle[roomSlot.point] && rtuBundle[roomSlot.point].length) {
parts.push("Room avg " + basAvgPositive(rtuBundle[roomSlot.point]) + "\u00B0");
}
hvacText = basHvacModePctSummary(hvacModeTimeline);
if (hvacText) {
parts.push(hvacText);
}
statsEl.textContent = parts.length ? parts.join(" · ") : "—";
}
// Fan spark card definition — chartInstance lets us destroy/redraw when live data arrives
var basDashboardResolved = {};
var basSparkRoleCards = {
fan: null,
power: null,
room: null
};
function basMergeDeviceData() {
var out = {};
for (var m = 0; m < arguments.length; m++) {
if (arguments[m]) {
Object.assign(out, arguments[m]);
}
}
return out;
}
function basSetSparkCardVisible(role, visible) {
var card = basSparkRoleCards[role];
if (!card) {
return;
}
var el = document.getElementById(card.id);
if (!el) {
return;
}
var col = el.closest("[class*='col-']");
if (col) {
col.style.display = visible ? "" : "none";
}
}
var sparkFanCard = {
id: "sparkFan",
valueId: "sparkFanValue",
subId: "sparkFanSub",
role: "fan",
cfg: { point: "", onThreshold: 0.5, isTemp: false, color: null },
type: "area",
chartInstance: null
};
var sparkPowerCard = {
id: "sparkPower",
valueId: "sparkPowerValue",
subId: "sparkPowerSub",
role: "power",
cfg: { point: "", onThreshold: 3, isTemp: false, color: null },
type: "area",
chartInstance: null
};
var sparkRoomCard = {
id: "sparkRoom",
valueId: "sparkRoomValue",
subId: "sparkRoomSub",
role: "room",
cfg: { point: "", isTemp: true, color: null },
type: "area",
chartInstance: null
};
basSparkRoleCards.fan = sparkFanCard;
basSparkRoleCards.power = sparkPowerCard;
basSparkRoleCards.room = sparkRoomCard;
// thin out thousands of points so the tiny spark chart stays fast
function basDownsampleSeries(samples, maxPts) {
maxPts = maxPts || 72;
if (!samples || !samples.length) return [];
if (samples.length <= maxPts) {
return samples.map(function(s) { return { x: s.x, y: parseFloat(s.y) }; });
}
var out = [];
var step = (samples.length - 1) / (maxPts - 1);
for (var i = 0; i < maxPts; i++) {
var idx = Math.round(i * step);
out.push({ x: samples[idx].x, y: parseFloat(samples[idx].y) });
}
return out;
}
var basForecastData = null;
var basForecastLoadPromise = null;
// Load forecast_output.json — P line chart + phase compare (on demand via Load forecast)
function basLoadForecast(done) {
if (basForecastLoadPromise) {
basForecastLoadPromise.finally(function() { if (done) done(); });
return basForecastLoadPromise;
}
basForecastLoadPromise = fetch("forecast_output.json?" + Date.now())
.then(function(res) { return res.ok ? res.json() : null; })
.then(function(json) { basForecastData = json; return json; })
.catch(function() { basForecastData = null; return null; });
basForecastLoadPromise.finally(function() { if (done) done(); });
return basForecastLoadPromise;
}
function basGetDashboardBundles() {
if ((basLiveRtuData && Object.keys(basLiveRtuData).length) ||
(basLiveVerisData && Object.keys(basLiveVerisData).length)) {
return { rtu: basLiveRtuData || {}, veris: basLiveVerisData || {} };
}
if (basSampleLoaded) {
var bundle = basGetSampleBundle();
return { rtu: bundle.rtu, veris: bundle.veris };
}
return null;
}
function basLoadForecastData() {
var btn = document.getElementById("basLoadForecastBtn");
if (btn) {
btn.disabled = true;
btn.textContent = "Loading forecast…";
}
return basLoadForecast().then(function(json) {
if (!json) {
basShowLiveStatus("error", "No forecast_output.json — run python forecast.py first.");
if (btn) {
btn.disabled = false;
btn.textContent = "Load forecast";
}
return;
}
var bundles = basGetDashboardBundles();
if (!bundles) {
basShowLiveStatus("warn", "Forecast loaded — load live or sample data to draw it on charts.");
if (btn) {
btn.disabled = false;
btn.textContent = "Load forecast";
}
return;
}
basApplyDashboard(bundles.rtu, bundles.veris);
basShowLiveStatus("ok", "Forecast overlay on P line and phase compare.");
if (btn) {
btn.textContent = "Forecast loaded";
}
});
}
// Last real point + future readings for the P line + phase compare dashed overlays
function basForecastSeries(actualData, pointName) {
if (!basForecastData) return null;
var entry = basForecastData[pointName] || (basForecastData.point === pointName ? basForecastData : null);
if (!entry || !entry.future || !entry.future.length) return null;
if (!actualData || !actualData.length) return entry.future;
return [actualData[actualData.length - 1]].concat(entry.future);
}
// runtime % - counts how many samples are above threshold (fan on, power draw, etc.)
function basOnPercent(samples, threshold) {
if (!samples || !samples.length) return 0;
var on = 0;
for (var i = 0; i < samples.length; i++) {
if (parseFloat(samples[i].y) > threshold) on++;
}
return Math.round((on / samples.length) * 1000) / 10;
}
// average temp - skips 0.00 startup readings for the big number
function basAvgPositive(samples) {
if (!samples || !samples.length) return 0;
var sum = 0, n = 0;
for (var i = 0; i < samples.length; i++) {
var v = parseFloat(samples[i].y);
if (v > 0) { sum += v; n++; }
}
return n ? Math.round((sum / n) * 10) / 10 : 0;
}
// builds a small Apex sparkline (no axes) into a div like #sparkFan
function basSparkChart(el, seriesData, color, chartType) {
return new ApexCharts(el, Object.assign({}, basApexTheme(), {
series: [{ data: seriesData }],
chart: {
type: chartType,
height: 52,
sparkline: { enabled: true },
background: "transparent",
animations: { enabled: false }
},
colors: [color],
stroke: { width: 2, curve: "smooth" },
fill: { opacity: 0.25 },
tooltip: { enabled: true, x: { show: false } },
xaxis: { type: "datetime", labels: { show: false }, axisBorder: { show: false } },
yaxis: { show: false }
}));
}
// Draw one spark from any [{x,y},...] array — works for SAMPLE (rtu-sample.js) or LIVE (getData)
function basRenderSparkCard(card, samples, colorIndex) {
var cfg = card.cfg;
if (!cfg || !cfg.point || !samples || !samples.length) {
return null;
}
var data = basDownsampleSeries(samples);
var color = cfg.color || basPointColor(cfg.point, colorIndex || 0);
var valueEl = document.getElementById(card.valueId);
if (valueEl) {
// Big number: runtime % for fan/power, avg temp for room
valueEl.textContent = cfg.isTemp
? basAvgPositive(samples) + "\u00B0"
: basOnPercent(samples, cfg.onThreshold) + "%";
}
var el = document.querySelector("#" + card.id);
if (!el || !data.length) {
return null;
}
// LIVE DATA — destroy old chart before redraw (sample chart replaced by live chart)
if (card.chartInstance) {
card.chartInstance.destroy();
}
card.chartInstance = basSparkChart(el, data, color, card.type);
card.chartInstance.render();
return card.chartInstance;
}
var basSampleLoaded = BAS_FORCE_SAMPLE;
var basSampleLoadPromise = null;
function basGetSampleBundle() {
return {
rtu: (typeof rtuData !== "undefined" && rtuData) ? rtuData : {},
veris: (typeof verisData !== "undefined" && verisData) ? verisData : {}
};
}
function basUnloadSampleScripts() {
var tags = document.querySelectorAll("script[data-bas-src]");
for (var i = 0; i < tags.length; i++) {
tags[i].parentNode.removeChild(tags[i]);
}
basSampleLoadPromise = null;
try { delete window.rtuData; } catch (e) { window.rtuData = undefined; }
try { delete window.verisData; } catch (e) { window.verisData = undefined; }
}
function basLoadScriptOnce(src, marker) {
return new Promise(function(resolve, reject) {
var bust = (src.indexOf("?") >= 0 ? "&" : "?") + "_=" + Date.now();
var tag = document.createElement("script");
tag.src = src + bust;
tag.setAttribute("data-bas-src", marker);
tag.onload = function() { resolve(); };
tag.onerror = function() { reject(new Error(src)); };
document.body.appendChild(tag);
});
}
function basEnsureSampleScripts() {
basUnloadSampleScripts();
basSampleLoadPromise = Promise.all([
basLoadScriptOnce("js/veris-sample.js", "veris-sample"),
basLoadScriptOnce("js/rtu-sample.js", "rtu-sample")
]);
return basSampleLoadPromise;
}
function basLoadSampleData() {
var btn = document.getElementById("basLoadSampleBtn");
if (btn) {
btn.disabled = true;
btn.textContent = "Loading sample…";
}
basShowLiveStatus("loading", "Loading sample data (rtu-sample.js + veris-sample.js)…");
return basEnsureSampleScripts().then(function() {
basSampleLoaded = true;
var bundle = basGetSampleBundle();
basApplyDashboard(bundle.rtu, bundle.veris);
basShowLiveStatus("sample", "Sample data loaded.");
if (btn) {
btn.disabled = false;
btn.textContent = "Load sample data";
}
}).catch(function() {
basShowLiveStatus("error", "Could not load sample .js files.");
if (btn) {
btn.disabled = false;
btn.textContent = "Load sample data";
}
});
}
// ----- CHART: APEX HEATMAP -> #chartCont -----
var chart = new ApexCharts(document.querySelector("#chartCont"), optHeatMap);
chart.render();
// ----- CHART: P LINE -> #lineChartCont (uses test.js colors) -----
var lineChart = new ApexCharts(document.querySelector("#lineChartCont"), Object.assign({}, basApexTheme(), basApexGridOff(), {
series: [{
name: "P",
data: []
}],
colors: [basPointColor("P", 0)],
chart: {
type: "line",
height: 300,
background: BAS_CHART_THEME.chart.background
},
stroke: { width: 2, curve: "smooth" },
markers: { size: 0, hover: { size: 4 } },
xaxis: {
type: "datetime",
labels: { datetimeUTC: false }
},
title: {
text: "Veris Meter - P"
}
}));
lineChart.render();
// ----- CHART: PHASE COMPARE Ia/Ib/Ic
var PHASE_COMPARE_TOGGLES = {
midnightLines: true, // dashed vertical lines at each midnight
extraAnnotations: true, // custom vertical markers in extraAnnotations array
fixedColors: true, // Ia/Ib/Ic colors instead of theme default
legendTopRight: true, // legend above chart, right-aligned
sharedTooltip: true // one tooltip showing all series at hovered time
};
// dashed vertical lines at each midnight on the chart
function chartMidnightAnnotations(samples) {
var lines = [];
if (!samples || samples.length < 2) return lines;
function localDayKey(ms) {
var d = new Date(ms);
return d.getFullYear() + "-" + d.getMonth() + "-" + d.getDate();
}
function localMidnightMs(ms) {
var d = new Date(ms);
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0).getTime();
}
var placed = {};
for (var i = 1; i < samples.length; i++) {
if (localDayKey(samples[i - 1].x) === localDayKey(samples[i].x)) continue;
var midnightMs = localMidnightMs(samples[i].x);
if (placed[midnightMs]) continue;
placed[midnightMs] = true;
var midnightDate = new Date(midnightMs);
lines.push({
x: midnightMs,
strokeDashArray: 6,
borderColor: BAS_CHART_THEME.annotation.line, // midnight line color from test.js
label: {
borderColor: BAS_CHART_THEME.annotation.line, // test.js
style: { color: BAS_CHART_THEME.annotation.labelText, background: BAS_CHART_THEME.annotation.labelBg, fontSize: "11px" }, // label colors from test.js
text: midnightDate.toLocaleDateString(undefined, { weekday: "short", month: "short", day: "numeric" })
}
});
}
return lines;
}
var phaseCompareExtraAnnotations = [
// { x: 1780572000000, strokeDashArray: 4, borderColor: "#FEB019", label: { text: "Noon spike" } }
];
var phaseCompareXAnnotations = [];
var phaseCompareOptions = Object.assign({}, basApexTheme(), { // dark theme from test.js
series: [],
colors: basCompareColors(["Ia", "Ib", "Ic"]),
chart: {
type: "line",
height: 300,
background: BAS_CHART_THEME.chart.background, // test.js
toolbar: { show: true }
},
grid: {
xaxis: {
lines: {
show: false
}
},
yaxis: {
lines: {
show: false
}
}
},
stroke: {
width: 2,
curve: "smooth"
},
markers: { size: 0, hover: { size: 4 } },
xaxis: {
type: "datetime",
// Keep false so midnight annotations align with 00:00 on the axis (not UTC-shifted)
labels: { datetimeUTC: false }
},
yaxis: {
title: { text: "Amps" }
},
title: {
text: "Veris Meter - Phase Current Comparison"
}
});
// fixedColors off -> drops basCompareColors() and Apex picks its own palette
if (!PHASE_COMPARE_TOGGLES.fixedColors) {
delete phaseCompareOptions.colors;
}
// legendTopRight - turn off via PHASE_COMPARE_TOGGLES.legendTopRight (or set show: false)
if (PHASE_COMPARE_TOGGLES.legendTopRight) {
phaseCompareOptions.legend = {
show: true,
position: "top",
horizontalAlign: "right"
};
}
// sharedTooltip - turn off via PHASE_COMPARE_TOGGLES.sharedTooltip
if (PHASE_COMPARE_TOGGLES.sharedTooltip) {
phaseCompareOptions.tooltip = {
shared: true,
intersect: false,
x: { format: "MMM d, HH:mm" }
};
}
// midnightLines / extraAnnotations - turn off via toggles; omit annotations key entirely when empty
if (phaseCompareXAnnotations.length) {
phaseCompareOptions.annotations = { xaxis: phaseCompareXAnnotations };
}
var phaseCompareChart = new ApexCharts(document.querySelector("#phaseCompareCont"), phaseCompareOptions);
phaseCompareChart.render();
// ----- CHART: TEMP VS SETPOINT COLUMN -> #radialBarCont -----
// makes timestamp -> setpoint value lookup so we can match actual to set at same x
function basLookupByX(samples) {
var map = {};
for (var i = 0; i < samples.length; i++) {
map[samples[i].x] = parseFloat(samples[i].y); // key = time, value = setpoint temp
}
return map;
}
// turns two sample arrays into bar chart data: average (actual - setpoint) per hour or day
function basTempDeviationSeries(actualSamples, setpointSamples) {
function dayKey(ms) {
var d = new Date(ms);
return d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, "0") + "-" + String(d.getDate()).padStart(2, "0");
}
function hourKey(ms) {
var d = new Date(ms);
return dayKey(ms) + " " + String(d.getHours()).padStart(2, "0") + ":00";
}
var setMap = basLookupByX(setpointSamples); // read setpoint values by timestamp
var bucketKeyFn = dayKey; // group samples into one bar per day
var bucketLabel = function(key) { return key; };
var buckets = {};
for (var a = 0; a < actualSamples.length; a++) {
var x = actualSamples[a].x; // timestamp must match between actual and setpoint
if (setMap[x] === undefined) continue; // skip if no setpoint at this time
var deviation = parseFloat(actualSamples[a].y) - setMap[x]; // actual temp minus setpoint
var key = bucketKeyFn(x);
if (!buckets[key]) {
buckets[key] = { sum: 0, count: 0 };
}
buckets[key].sum += deviation;
buckets[key].count++;
}
var keys = Object.keys(buckets).sort();
if (keys.length < 3) {
// not enough days - switch to hourly bars instead
buckets = {};
bucketKeyFn = hourKey;
bucketLabel = function(key) { return key.split(" ")[1]; };
for (var h = 0; h < actualSamples.length; h++) {
var hx = actualSamples[h].x;
if (setMap[hx] === undefined) continue;
var hdev = parseFloat(actualSamples[h].y) - setMap[hx];
var hkey = bucketKeyFn(hx);
if (!buckets[hkey]) {
buckets[hkey] = { sum: 0, count: 0 };
}
buckets[hkey].sum += hdev;
buckets[hkey].count++;
}
keys = Object.keys(buckets).sort();
}
return {
categories: keys.map(bucketLabel), // x-axis labels (times)
values: keys.map(function(key) {
return Math.round((buckets[key].sum / buckets[key].count) * 100) / 100; // bar height
})
};
}
var tempDevSeries = { categories: [], values: [] };
var tempDevTitle = "RTU - temp vs setpoint (no data)";
var tempPair = null;
// center 0 in the middle of y-axis (symmetric above/below setpoint)
var tempDevLimit = 3;
for (var tl = 0; tl < tempDevSeries.values.length; tl++) {
tempDevLimit = Math.max(tempDevLimit, Math.abs(tempDevSeries.values[tl]));
}
tempDevLimit = Math.ceil(tempDevLimit);
// draw bars into #radialBarCont
tempDevChart = new ApexCharts(document.querySelector("#radialBarCont"), Object.assign({}, basApexTheme(), {
series: [{
name: tempPair ? tempPair.actual + " - " + tempPair.setpoint : "Deviation",
data: tempDevSeries.values // bar heights: degrees above/below setpoint
}],
chart: {
type: "bar",
height: 350,
background: BAS_CHART_THEME.chart.background
},
grid: {
xaxis: {
lines: {
show: false
}
},
yaxis: {
lines: {
show: true //horizontal axis lines
}
}
},
plotOptions: {
bar: {
colors: {
ranges: [
{ from: -100, to: -3, color: "#0967d3" }, // well below setpoint
{ from: -2.99, to: 0, color: "#17a7fa" }, // slightly below setpoint
{ from: 0.01, to: 100, color: basPointColor("cooling", 0) } // above setpoint
]
},
columnWidth: "80%"
}
},
dataLabels: { enabled: false },
yaxis: {
min: -tempDevLimit, // 0 = at setpoint, negative = below, positive = above
max: tempDevLimit,
forceNiceScale: false,
title: { text: "Deviation from setpoint" },
labels: {
formatter: function(y) {
return (y > 0 ? "+" : "") + y.toFixed(1) + "\u00B0";
}
}
},
xaxis: {
type: "category",
categories: tempDevSeries.categories,
labels: { rotate: -90 }
},
tooltip: {
y: {
formatter: function(val) {
return (val > 0 ? "+" : "") + val.toFixed(2) + "\u00B0 from setpoint";
}
}
},
annotations: {
yaxis: [{
y: 0, // dashed line = setpoint (zero deviation)
borderColor: BAS_CHART_THEME.ui.text,
strokeDashArray: 4,
label: {
text: "Setpoint",
style: { color: BAS_CHART_THEME.annotation.labelText, background: BAS_CHART_THEME.annotation.labelBg }
}
}]
},
title: { text: tempDevTitle }
}));
tempDevChart.render();
function basApplyLiveTempDevChart(deviceData, pair) {
if (!tempDevChart || !deviceData) {
return;
}
pair = pair || basResolveTempPair(basSeriesKeys(deviceData), BAS_CHART_TEMP_PAIRS);
if (!pair || !deviceData[pair.actual] || !deviceData[pair.actual].length || !deviceData[pair.setpoint] || !deviceData[pair.setpoint].length) {
return;
}
var series = basTempDeviationSeries(deviceData[pair.actual], deviceData[pair.setpoint]);
var limit = 3;
for (var tl = 0; tl < series.values.length; tl++) {
limit = Math.max(limit, Math.abs(series.values[tl]));
}
limit = Math.ceil(limit);
var title = pair.label + " (" + pair.actual + " vs " + pair.setpoint + ")";
tempDevChart.updateSeries([{
name: pair.actual + " - " + pair.setpoint,
data: series.values
}], false);
tempDevChart.updateOptions({
xaxis: { categories: series.categories },
yaxis: { min: -limit, max: limit },
title: { text: title }
}, false, false);
}
// ----- CHART: HVAC MODE TIMELINE -> #hvacModeCont -----
// rtuData.HVACMode serial data -> horizontal rangeBar segments per mode hold
var HVAC_MODE_LABELS = {
0: "Startup",
3: "Cooling",
7: "Off"
// add more codes here when you confirm labels in Desigo
};
var HVAC_MODE_COLORS = {
0: "#808080",
3: basPointColor("cooling", 0),
7: "#6c757d"
};
function basHvacModeLabel(modeVal) {
var key = Math.round(parseFloat(modeVal));
return HVAC_MODE_LABELS[key] || ("Mode " + modeVal);
}
function basHvacModeColor(modeVal) {
var key = Math.round(parseFloat(modeVal));
return HVAC_MODE_COLORS[key] || basPointColor("HVACMode", key);
}
function basFormatDuration(msStart, msEnd) {
var diffMs = Math.max(0, msEnd - msStart);
var hours = diffMs / (1000 * 60 * 60);
if (hours >= 24) {
var days = Math.round((hours / 24) * 10) / 10;
return days + (days === 1 ? " day" : " days");
}
if (hours >= 1) {
var h = Math.round(hours * 10) / 10;
return h + (h === 1 ? " hr" : " hrs");
}
var mins = Math.round(diffMs / (1000 * 60));
return mins + (mins === 1 ? " min" : " mins");
}
// each sample holds until the next timestamp; merge adjacent spans with same mode
function basHvacModeTimeline(samples) {
if (!samples || samples.length < 2) {
return [];
}
var bars = [];
for (var i = 0; i < samples.length - 1; i++) {
var segStart = samples[i].x;
var segEnd = samples[i + 1].x;
var segMode = parseFloat(samples[i].y);
if (segEnd <= segStart) {
continue;
}
var label = basHvacModeLabel(segMode);
var last = bars[bars.length - 1];
if (last && last.modeCode === segMode && last.y[1] === segStart) {
last.y[1] = segEnd;
} else {
bars.push({
x: label,
y: [segStart, segEnd],
fillColor: basHvacModeColor(segMode),
modeCode: segMode,
modeLabel: label
});
}
}
return bars;
}
var hvacModeChart = null;
var hvacModeTimeline = [];
// Draw or redraw HVAC timeline from [{x,y},...] — SAMPLE (rtu-sample.js) or LIVE (getData HVACMode)
function basRenderHvacModeChart(samples) {
hvacModeTimeline = basHvacModeTimeline(samples || []);
var options = Object.assign({}, basApexTheme(), {
series: [{
name: "HVACMode",
data: hvacModeTimeline
}],
chart: {
type: "rangeBar",
height: 350,
background: BAS_CHART_THEME.chart.background,
animations: { enabled: false }
},
plotOptions: {
bar: {
horizontal: true,
distributed: true,
rangeBarGroupRows: false,
barHeight: "70%",
dataLabels: {
hideOverflowingLabels: true
}
}
},
dataLabels: {
enabled: true,
formatter: function(val, opts) {
var pt = hvacModeTimeline[opts.dataPointIndex];
if (!pt) {
return "";
}
var ms = val[1] - val[0];
if (ms < 20 * 60 * 1000) {
return "";
}
return basFormatDuration(val[0], val[1]);
},
style: {
colors: ["#fff"],
fontSize: "11px",
fontWeight: 600
},
dropShadow: { enabled: false }
},
xaxis: {
type: "datetime",
labels: { datetimeUTC: false }
},
yaxis: {
show: true,
labels: {
style: { colors: BAS_CHART_THEME.ui.text }
}
},
grid: {
xaxis: { lines: { show: false } },
yaxis: { lines: { show: false } },
row: {
colors: [BAS_CHART_THEME.chart.background, "#3a3a3c"],
opacity: 1
}
},
tooltip: {
custom: function(opts) {
var pt = hvacModeTimeline[opts.dataPointIndex];
if (!pt) {
return "";
}
var start = new Date(pt.y[0]);
var end = new Date(pt.y[1]);
return "<div class=\"apexcharts-tooltip-rangebar\">" +
"<span><b>" + (pt.modeLabel || pt.x) + "</b> (" + pt.modeCode + ")</span><br>" +
"<span>" + start.toLocaleString() + " " + end.toLocaleString() + "</span><br>" +
"<span>" + basFormatDuration(pt.y[0], pt.y[1]) + "</span>" +
"</div>";
}
},
legend: { show: false },
title: { text: "RTU - HVAC Mode Timeline" }
});
// LIVE DATA — destroy sample chart before redraw (same pattern as fan spark)
if (hvacModeChart) {
hvacModeChart.destroy();
}
hvacModeChart = new ApexCharts(document.querySelector("#hvacModeCont"), options);
hvacModeChart.render();
}
// SAMPLE path: rtu-sample.js HVACMode //can be deleted just to show data
// ----- CHART: FAN STARTS (TRIGGER) -> #TriggerCont -----
// count fan starts (0->1) into every hour or day in the sample range — empty buckets stay 0
function basFanStartCountSeries(samples, threshold) {
if (!samples || samples.length < 2) {
return [];
}
function hourFloor(ms) {
var d = new Date(ms);
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours(), 0, 0, 0).getTime();
}
function dayFloor(ms) {
var d = new Date(ms);
return new Date(d.getFullYear(), d.getMonth(), d.getDate(), 0, 0, 0, 0).getTime();
}
var sorted = samples.slice().sort(function(a, b) { return a.x - b.x; });
var minMs = sorted[0].x;
var maxMs = sorted[sorted.length - 1].x;
var spanHours = (maxMs - minMs) / (1000 * 60 * 60);
var useHourly = spanHours <= 48;
var stepMs = useHourly ? 3600000 : 86400000;
var bucketFloor = useHourly ? hourFloor : dayFloor;
var start = bucketFloor(minMs);
var end = bucketFloor(maxMs);
var buckets = {};
var times = [];
for (var t = start; t <= end; t += stepMs) {
times.push(t);
buckets[t] = 0;
}
for (var i = 1; i < sorted.length; i++) {
var prev = parseFloat(sorted[i - 1].y);
var curr = parseFloat(sorted[i].y);
if (prev <= threshold && curr > threshold) {
var b = bucketFloor(sorted[i].x);
if (buckets[b] !== undefined) {
buckets[b]++;
}
}
}
return times.map(function(ts) {
return { x: ts, y: buckets[ts] };
});
}
var triggerChart = null;
function basRenderTriggerChart(samples, slot) {
var threshold = (slot && slot.onThreshold != null) ? slot.onThreshold : 0.5;
var titleText = (slot && slot.point)
? slot.label + " (" + slot.point + ")"
: "Fan starts";
var counts = basFanStartCountSeries(samples, threshold);
var options = Object.assign({}, basApexTheme(), {
series: [{
name: "Starts",
data: counts
}],
colors: ["#FEB019"],
chart: {
type: "bar",
height: 350,
background: BAS_CHART_THEME.chart.background,
toolbar: { show: true }
},
plotOptions: {
bar: {
borderRadius: 4,
columnWidth: "85%",
dataLabels: {
position: "top"
}
}
},
dataLabels: {
enabled: true,
formatter: function(val) {
return val > 0 ? val : "";
},
offsetY: -16,
style: {
fontSize: "11px",
colors: [BAS_CHART_THEME.ui.text]
}
},
xaxis: {
type: "datetime",
labels: {
datetimeUTC: false,
rotate: -45,
style: { colors: BAS_CHART_THEME.ui.text }
}
},
yaxis: {
min: 0,
forceNiceScale: true,
title: { text: "Starts per period" },
labels: {
formatter: function(val) {
return Math.round(val);
}
}
},
title: {
text: titleText
},
tooltip: {
x: { format: "MMM d, HH:mm" },
y: {
formatter: function(val) {
return val + (val === 1 ? " start" : " starts");
}
}
}
});
if (triggerChart) {
triggerChart.destroy();
}
triggerChart = new ApexCharts(document.querySelector("#TriggerCont"), options);
triggerChart.render();
}
var basLiveRtuData = null;
var basLiveVerisData = null;
// RTU charts (fan, room, HVAC, temp) vs Veris charts (heatmap, power, P line, phases)
function basApplyDashboard(rtuBundle, verisBundle) {
var roles, role, slot, card, pt, pts, hi, keys, heatSeries, series, annotations, forecastPts, lineOpts;
var rtuResolved, verisResolved, phaseColors, phaseDash, hasPhaseForecast, phaseOpts, actualCount;
rtuBundle = rtuBundle || {};
verisBundle = verisBundle || {};
rtuResolved = basResolveDashboard(rtuBundle);
verisResolved = basResolveDashboard(verisBundle);
basDashboardResolved = { rtu: rtuResolved, veris: verisResolved };
function basApplySpark(role, resolved, bundle, colorIndex) {
slot = resolved["spark_" + role];
card = basSparkRoleCards[role];
if (slot && card && bundle[slot.point] && bundle[slot.point].length) {
card.cfg = {
point: slot.point,
onThreshold: slot.onThreshold != null ? slot.onThreshold : 0.5,
isTemp: !!slot.isTemp,
color: null
};
if (card.subId) {
subEl = document.getElementById(card.subId);
if (subEl) {
subEl.textContent = slot.point;
}
}
labelEl = document.getElementById(card.id);
if (labelEl) {
labelWrap = labelEl.closest(".spark-card");
if (labelWrap) {
sparkLabel = labelWrap.querySelector(".spark-label");
if (sparkLabel && slot.label) {
sparkLabel.textContent = slot.label;
}
}
}
basRenderSparkCard(card, bundle[slot.point], colorIndex);
basSetSparkCardVisible(role, true);
} else if (card) {
basSetSparkCardVisible(role, false);
}
}
var subEl, labelEl, labelWrap, sparkLabel;
basApplySpark("fan", rtuResolved, rtuBundle, 0);
basApplySpark("room", rtuResolved, rtuBundle, 2);
basApplySpark("power", verisResolved, verisBundle, 1);
setChartVisible("sparks", !!(
(rtuResolved.spark_fan && rtuBundle[rtuResolved.spark_fan.point]) ||
(rtuResolved.spark_room && rtuBundle[rtuResolved.spark_room.point]) ||
(verisResolved.spark_power && verisBundle[verisResolved.spark_power.point])
));
slot = rtuResolved.trigger_count;
if (slot && rtuBundle[slot.point] && rtuBundle[slot.point].length) {
basRenderTriggerChart(rtuBundle[slot.point], slot);
setChartVisible("trigger", true);
} else {
setChartVisible("trigger", false);
}
slot = rtuResolved.hvac_mode;
if (slot && rtuBundle[slot.point] && rtuBundle[slot.point].length) {
basRenderHvacModeChart(rtuBundle[slot.point]);
setChartVisible("hvacMode", true);
} else {
setChartVisible("hvacMode", false);
}
slot = rtuResolved.temp_deviation;
if (slot && slot.pair) {
basApplyLiveTempDevChart(rtuBundle, slot.pair);
setChartVisible("radialBar", true);
} else {
setChartVisible("radialBar", false);
}
slot = verisResolved.line_power;
if (slot && lineChart && verisBundle[slot.point] && verisBundle[slot.point].length) {
pt = slot.point;
series = [{ name: pt, data: verisBundle[pt] }];
forecastPts = basForecastSeries(verisBundle[pt], pt);
if (forecastPts) {
series.push({ name: pt + " forecast", data: forecastPts });
}
lineChart.updateSeries(series, false);
lineOpts = {
title: { text: BAS_LIVE.verisDevice + " - " + pt + (forecastPts ? " (+ forecast)" : "") },
colors: forecastPts ? [basPointColor(pt, 0), "#d4526e"] : [basPointColor(pt, 0)],
stroke: forecastPts
? { width: [2, 2], curve: "smooth", dashArray: [0, 6] }
: { width: 2, curve: "smooth" },
annotations: forecastPts ? {
xaxis: [{
x: verisBundle[pt][verisBundle[pt].length - 1].x,
strokeDashArray: 4,
borderColor: BAS_CHART_THEME.annotation.line,
label: {
borderColor: BAS_CHART_THEME.annotation.line,
style: {
color: BAS_CHART_THEME.annotation.labelText,
background: BAS_CHART_THEME.annotation.labelBg,
fontSize: "11px"
},
text: "Forecast →"
}
}]
} : { xaxis: [] }
};
lineChart.updateOptions(lineOpts, false, false);
setChartVisible("lineP", true);
} else {
setChartVisible("lineP", false);
}
slot = verisResolved.heatmap;
if (slot && chart) {
keys = slot.points;
heatSeries = [];
for (hi = 0; hi < keys.length; hi++) {
if (verisBundle[keys[hi]] && verisBundle[keys[hi]].length) {
heatSeries.push({ name: keys[hi], data: verisBundle[keys[hi]] });
}
}
if (heatSeries.length) {
chart.updateSeries(heatSeries, false);
chart.updateOptions({
title: { text: BAS_LIVE.verisDevice + " - All Points HeatMap" }
}, false, false);
}
setChartVisible("heatmap", heatSeries.length > 0 && basIsChartToggleChecked("heatmap"));
} else {
setChartVisible("heatmap", false);
}
slot = verisResolved.phase_compare;
if (slot && phaseCompareChart) {
pts = slot.points;
series = [];
phaseColors = [];
phaseDash = [];
hasPhaseForecast = false;
actualCount = 0;
for (hi = 0; hi < pts.length; hi++) {
if (verisBundle[pts[hi]] && verisBundle[pts[hi]].length) {
actualCount++;
pt = pts[hi];
series.push({ name: pt, data: verisBundle[pt] });
phaseColors.push(basPointColor(pt, hi));
phaseDash.push(0);
forecastPts = basForecastSeries(verisBundle[pt], pt);
if (forecastPts) {
hasPhaseForecast = true;
series.push({ name: pt + " forecast", data: forecastPts });
phaseColors.push(basPointColor(pt, hi));
phaseDash.push(6);
}
}
}
if (actualCount === pts.length) {
phaseCompareChart.updateSeries(series, false);
phaseOpts = {
colors: phaseColors,
title: { text: BAS_LIVE.verisDevice + " - Phase compare" + (hasPhaseForecast ? " (+ forecast)" : "") },
stroke: hasPhaseForecast
? { width: 2, curve: "smooth", dashArray: phaseDash }
: { width: 2, curve: "smooth" }
};
phaseCompareChart.updateOptions(phaseOpts, false, false);
annotations = [];
if (PHASE_COMPARE_TOGGLES.midnightLines) {
annotations = chartMidnightAnnotations(verisBundle[pts[0]]);
if (PHASE_COMPARE_TOGGLES.extraAnnotations) {
annotations = annotations.concat(phaseCompareExtraAnnotations);
}
}
if (hasPhaseForecast) {
annotations.push({
x: verisBundle[pts[0]][verisBundle[pts[0]].length - 1].x,
strokeDashArray: 4,
borderColor: BAS_CHART_THEME.annotation.line,
label: {
borderColor: BAS_CHART_THEME.annotation.line,
style: {
color: BAS_CHART_THEME.annotation.labelText,
background: BAS_CHART_THEME.annotation.labelBg,
fontSize: "11px"
},
text: "Forecast →"
}
});
}
if (annotations.length) {
phaseCompareChart.updateOptions({ annotations: { xaxis: annotations } }, false, false);
} else {
phaseCompareChart.updateOptions({ annotations: { xaxis: [] } }, false, false);
}
setChartVisible("phaseCompare", true);
} else {
setChartVisible("phaseCompare", false);
}
} else {
setChartVisible("phaseCompare", false);
}
basUpdateDashboardSummary(rtuBundle, verisBundle, rtuResolved, verisResolved);
}
function basApplyLiveDashboard() {
if ((!basLiveRtuData || !Object.keys(basLiveRtuData).length) &&
(!basLiveVerisData || !Object.keys(basLiveVerisData).length)) {
return;
}
basApplyDashboard(basLiveRtuData, basLiveVerisData);
}
function basLoadLiveRtuCharts() {
if (!BAS_LIVE.keyid) {
return;
}
requestAnimationFrame(function() {
basGetData(BAS_LIVE.keyid, BAS_LIVE.fanDevice, function(err, jsonData) {
if (err || !jsonData) {
basLiveFetchState.rtu = "fail";
console.warn("basLoadLiveRtuCharts", err, jsonData);
} else {
basLiveRtuData = jsonData;
basLiveFetchState.rtu = "ok";
basApplyLiveDashboard();
}
basRefreshLiveStatusBar();
});
});
}
function basLoadLiveVerisCharts() {
if (!BAS_LIVE.keyid) {
return;
}
requestAnimationFrame(function() {
basGetData(BAS_LIVE.keyid, BAS_LIVE.verisDevice, function(err, jsonData) {
if (err || !jsonData) {
basLiveFetchState.veris = "fail";
console.warn("basLoadLiveVerisCharts", err, jsonData);
} else {
basLiveVerisData = jsonData;
basLiveFetchState.veris = "ok";
basApplyLiveDashboard();
}
basRefreshLiveStatusBar();
});
});
}
function basIsChartToggleChecked(chartId) {
var toggle = document.getElementById("toggle-" + chartId);
return !toggle || toggle.checked;
}
function setChartVisible(id, visible) {
var box = document.getElementById("chartBox-" + id);
if (box) {
box.style.display = visible ? "" : "none";
}
}
function basSyncChartToggle(chartId, visible) {
var toggle = document.getElementById("toggle-" + chartId);
if (toggle) {
toggle.checked = visible;
}
setChartVisible(chartId, visible);
}
function basAllChartIds() {
var boxes = document.querySelectorAll("[data-chart]");
var ids = [];
for (var i = 0; i < boxes.length; i++) {
ids.push(boxes[i].getAttribute("data-chart"));
}
return ids;
}
function basShowAllCharts() {
var ids = basAllChartIds();
for (var i = 0; i < ids.length; i++) {
basSyncChartToggle(ids[i], true);
}
}
// index.php snapshot toolbar -> postMessage -> show/hide Device Snapshot charts
function basApplySnapshotChartCommand(msg) {
var charts = msg.charts || [];
var action = msg.action || "only";
if (action === "showAll") {
if (document.getElementById("chartsShowAll")) {
document.getElementById("chartsShowAll").click();
} else {
basShowAllCharts();
}
return;
}
if (action === "hideAll") {
if (document.getElementById("chartsHideAll")) {
document.getElementById("chartsHideAll").click();
} else {
var hideIds = basAllChartIds();
for (var hi = 0; hi < hideIds.length; hi++) {
basSyncChartToggle(hideIds[hi], false);
}
}
return;
}
if (action === "set") {
for (var si = 0; si < charts.length; si++) {
basSyncChartToggle(charts[si], !!msg.visible);
}
return;
}
if (action === "only") {
var allIds = basAllChartIds();
for (var i = 0; i < allIds.length; i++) {
basSyncChartToggle(allIds[i], charts.indexOf(allIds[i]) >= 0);
}
if (charts.length) {
var box = document.getElementById("chartBox-" + charts[0]);
if (box) {
box.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
}
}
}
window.addEventListener("message", function(ev) {
if (!ev.data || ev.data.type !== "basSnapshotChart") {
return;
}
basApplySnapshotChartCommand(ev.data);
});
var chartToggles = document.querySelectorAll(".chart-load-toggle");
for (var t = 0; t < chartToggles.length; t++) {
setChartVisible(chartToggles[t].getAttribute("data-chart"), chartToggles[t].checked);
chartToggles[t].addEventListener("change", function() {
setChartVisible(this.getAttribute("data-chart"), this.checked);
});
}
var chartsHideAll = document.getElementById("chartsHideAll");
if (chartsHideAll) {
chartsHideAll.addEventListener("click", function() {
for (var h = 0; h < chartToggles.length; h++) {
chartToggles[h].checked = false;
setChartVisible(chartToggles[h].getAttribute("data-chart"), false);
}
});
}
var chartsShowAll = document.getElementById("chartsShowAll");
if (chartsShowAll) {
chartsShowAll.addEventListener("click", function() {
for (var s = 0; s < chartToggles.length; s++) {
chartToggles[s].checked = true;
setChartVisible(chartToggles[s].getAttribute("data-chart"), true);
}
});
}
var basLoadSampleBtn = document.getElementById("basLoadSampleBtn");
if (basLoadSampleBtn) {
basLoadSampleBtn.addEventListener("click", function() {
basLoadSampleData();
});
}
var basLoadForecastBtn = document.getElementById("basLoadForecastBtn");
if (basLoadForecastBtn) {
basLoadForecastBtn.addEventListener("click", function() {
basLoadForecastData();
});
}
(function basInitDashboard() {
basInitLiveStatusBar();
if (BAS_LIVE.keyid) {
basLoadLiveRtuCharts();
basLoadLiveVerisCharts();
} else if (BAS_FORCE_SAMPLE) {
basLoadSampleData();
}
if (BAS_FORCE_FORECAST) {
basLoadForecastData();
}
})();
</script>
</body>
</html>