1788 lines
57 KiB
PHP
1788 lines
57 KiB
PHP
<?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>
|