382 lines
16 KiB
HTML
382 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>DVgo Device Emulator</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||
<link
|
||
href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=Outfit:wght@500;600;700&display=swap"
|
||
rel="stylesheet"
|
||
/>
|
||
<link rel="stylesheet" href="/styles.css" />
|
||
</head>
|
||
<body>
|
||
<div id="login-overlay" class="overlay hidden">
|
||
<form id="login-form" class="login-card" autocomplete="on">
|
||
<p class="eyebrow">Control portal</p>
|
||
<h1>Sign in</h1>
|
||
<p class="muted">Basic auth for the emulator API. Credentials stay in this browser session.</p>
|
||
<label>
|
||
Username
|
||
<input id="login-user" name="username" required />
|
||
</label>
|
||
<label>
|
||
Password
|
||
<input id="login-pass" name="password" type="password" required />
|
||
</label>
|
||
<p id="login-error" class="error hidden"></p>
|
||
<button type="submit">Continue</button>
|
||
</form>
|
||
</div>
|
||
|
||
<header class="topbar">
|
||
<div>
|
||
<h1>DVgo device emulator</h1>
|
||
</div>
|
||
<div class="topbar-meta">
|
||
<div id="local-time" class="local-time">Current local time: --:--:--</div>
|
||
<div id="tb-status" class="status-pill off">ThingsBoard</div>
|
||
<div id="mqtt-status" class="status-pill off">MQTT</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="layout">
|
||
<aside class="sidebar">
|
||
<div class="panel-head">
|
||
<div class="panel-title">
|
||
<h2>Devices</h2>
|
||
<span id="device-count" class="device-count">0/100</span>
|
||
</div>
|
||
<div class="device-menu">
|
||
<button
|
||
id="device-menu-toggle"
|
||
type="button"
|
||
class="device-menu-button"
|
||
aria-haspopup="menu"
|
||
aria-expanded="false"
|
||
aria-controls="device-menu"
|
||
aria-label="Device options"
|
||
>
|
||
<span class="device-menu-icon" aria-hidden="true"></span>
|
||
</button>
|
||
<div id="device-menu" class="device-menu-panel hidden" role="menu">
|
||
<button id="add-open" type="button" class="primary" role="menuitem">Add</button>
|
||
<button id="select-all" type="button" role="menuitem">Select all</button>
|
||
<button id="deselect-all" type="button" role="menuitem">Deselect all</button>
|
||
<button id="bulk-disable" type="button" role="menuitem">Disable selected</button>
|
||
<button id="bulk-enable" type="button" role="menuitem">Enable selected</button>
|
||
<button id="bulk-delete" type="button" class="danger" role="menuitem">Delete selected</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<p id="bulk-status" class="muted"></p>
|
||
<div id="device-list" class="device-list"></div>
|
||
</aside>
|
||
|
||
<section class="workspace">
|
||
<div id="empty-state" class="empty-state">
|
||
<h2>No device selected</h2>
|
||
<p>Add a virtual DVgo device, then use the dashboard to publish live *_s telemetry to ThingsBoard.</p>
|
||
</div>
|
||
|
||
<div id="dashboard" class="dashboard hidden">
|
||
<div class="dash-head">
|
||
<div>
|
||
<p id="dash-type" class="eyebrow"></p>
|
||
<h2 id="dash-name"></h2>
|
||
<p id="dash-id" class="mono muted"></p>
|
||
</div>
|
||
<button id="delete-one" type="button" class="danger">Delete</button>
|
||
</div>
|
||
|
||
<div class="control-block">
|
||
<button id="toggle-mqtt" type="button" class="primary">Connect</button>
|
||
<span id="mqtt-spinner" class="spinner spinner-inline hidden" aria-hidden="true"></span>
|
||
<p class="connection-response">
|
||
connection response:
|
||
<span id="connection-response">—</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div class="control-block">
|
||
<button id="toggle-telemetry" type="button" class="primary">Start telemetry</button>
|
||
</div>
|
||
|
||
<section class="group plant-panel">
|
||
<h3>Virtual Diverter</h3>
|
||
<p class="muted">Follows ThingsBoard shared attributes from the apps: one-shot boost (boostNowHw / boostNowAux) and HW/aux schedules. Schedule saves are polled every 5s while MQTT is connected.</p>
|
||
<section class="plant-card">
|
||
<div class="plant-card-head">
|
||
<h4>Solar</h4>
|
||
<span class="lamp" data-power-lamp="solar" aria-hidden="true"></span>
|
||
<output class="plant-status" data-plant-status="solar"></output>
|
||
</div>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Power Supply (W)</span>
|
||
<output data-power-value="solar">0</output>
|
||
</div>
|
||
<label>
|
||
New value (W)
|
||
<input id="power-solar" data-power-input="solar" type="number" min="0" max="100000" step="1" value="0" />
|
||
</label>
|
||
<button type="button" data-power-update="solar">Update</button>
|
||
</div>
|
||
</section>
|
||
<section class="plant-card">
|
||
<div class="plant-card-head">
|
||
<h4>Grid</h4>
|
||
<span class="lamp" data-power-lamp="grid" aria-hidden="true"></span>
|
||
<output class="plant-status" data-plant-status="grid"></output>
|
||
</div>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Power Draw (W)</span>
|
||
<output data-power-value="grid">0</output>
|
||
</div>
|
||
<p class="muted plant-note">Calculated from solar, home load, hot water, aux, and battery.</p>
|
||
</div>
|
||
</section>
|
||
<section class="plant-card">
|
||
<div class="plant-card-head">
|
||
<h4>Home load</h4>
|
||
<span class="lamp" data-power-lamp="home" aria-hidden="true"></span>
|
||
<output class="plant-status" data-plant-status="home"></output>
|
||
</div>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Power Draw (W)</span>
|
||
<output data-power-value="home">0</output>
|
||
</div>
|
||
<label>
|
||
New value (W)
|
||
<input id="power-home" data-power-input="home" type="number" min="0" max="100000" step="1" value="0" />
|
||
</label>
|
||
<button type="button" data-power-update="home">Update</button>
|
||
</div>
|
||
</section>
|
||
<section class="plant-card">
|
||
<div class="plant-card-head">
|
||
<h4>Hot water</h4>
|
||
<span id="hw-lamp" class="lamp" aria-hidden="true"></span>
|
||
<output class="plant-status" data-plant-status="hotWater"></output>
|
||
</div>
|
||
<div class="schedule-board" id="hw-schedule">
|
||
<div class="schedule-line" data-schedule-line="hw-0">
|
||
<span class="schedule-label">Schedule 0</span>
|
||
<span class="schedule-times" data-schedule-times="hw-0">— – —</span>
|
||
</div>
|
||
<div class="schedule-line" data-schedule-line="hw-1">
|
||
<span class="schedule-label">Schedule 1</span>
|
||
<span class="schedule-times" data-schedule-times="hw-1">— – —</span>
|
||
</div>
|
||
<p class="schedule-note" data-schedule-note="hw"></p>
|
||
</div>
|
||
<button type="button" id="hw-boost" class="plant-boost primary">Hot Water Boost Start</button>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Tank Temp °C</span>
|
||
<output id="tank-temp">—</output>
|
||
</div>
|
||
<label>
|
||
New value
|
||
<input id="tank-temp-input" type="number" min="0" max="99" step="0.1" />
|
||
</label>
|
||
<button type="button" data-plant-update="tank">Update</button>
|
||
</div>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Heat Rate (°C/min)</span>
|
||
<output id="heat-rate-current">1</output>
|
||
</div>
|
||
<label>
|
||
New value
|
||
<input id="heat-rate" type="number" min="0" max="20" step="1" />
|
||
</label>
|
||
<button type="button" data-plant-update="heat">Update</button>
|
||
</div>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Thermostat max (°C)</span>
|
||
<output id="thermostat-current">65</output>
|
||
</div>
|
||
<label>
|
||
New value
|
||
<input id="thermostat" type="number" min="0" max="99" step="1" />
|
||
</label>
|
||
<button type="button" data-plant-update="thermostat">Update</button>
|
||
</div>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Power Draw (W)</span>
|
||
<output data-power-value="hotWater">0</output>
|
||
</div>
|
||
<label>
|
||
New value
|
||
<input id="power-hot-water" data-power-input="hotWater" type="number" min="0" max="100000" step="1" value="1000" />
|
||
</label>
|
||
<button type="button" data-power-update="hotWater">Update</button>
|
||
</div>
|
||
</section>
|
||
<section class="plant-card">
|
||
<div class="plant-card-head">
|
||
<h4>Aux</h4>
|
||
<span id="aux-lamp" class="lamp" aria-hidden="true"></span>
|
||
<output class="plant-status" data-plant-status="aux"></output>
|
||
</div>
|
||
<div class="schedule-board" id="aux-schedule">
|
||
<div class="schedule-line" data-schedule-line="aux-0">
|
||
<span class="schedule-label">Schedule 0</span>
|
||
<span class="schedule-times" data-schedule-times="aux-0">— – —</span>
|
||
</div>
|
||
<div class="schedule-line" data-schedule-line="aux-1">
|
||
<span class="schedule-label">Schedule 1</span>
|
||
<span class="schedule-times" data-schedule-times="aux-1">— – —</span>
|
||
</div>
|
||
<p class="schedule-note" data-schedule-note="aux"></p>
|
||
</div>
|
||
<button type="button" id="aux-boost" class="plant-boost primary">Aux Boost Start</button>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Power Draw (W)</span>
|
||
<output data-power-value="aux">0</output>
|
||
</div>
|
||
<label>
|
||
New value
|
||
<input id="power-aux" data-power-input="aux" type="number" min="0" max="100000" step="1" value="1000" />
|
||
</label>
|
||
<button type="button" data-power-update="aux">Update</button>
|
||
</div>
|
||
</section>
|
||
<section class="plant-card">
|
||
<div class="plant-card-head">
|
||
<h4>Battery</h4>
|
||
<span id="batt-lamp" class="lamp" aria-hidden="true"></span>
|
||
<output class="plant-status" data-plant-status="battery"></output>
|
||
</div>
|
||
<button type="button" id="batt-toggle" class="plant-boost primary">Battery Off</button>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Power rate (W / min)</span>
|
||
<output id="charge-rate-current">500</output>
|
||
</div>
|
||
<label>
|
||
New value
|
||
<input id="charge-rate" type="number" min="0" max="100000" step="1" />
|
||
</label>
|
||
<button type="button" data-plant-update="charge">Update</button>
|
||
</div>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Capacity (W)</span>
|
||
<output id="battery-total-current">5000</output>
|
||
</div>
|
||
<label>
|
||
New value
|
||
<input id="battery-total" type="number" min="0" max="100000" step="1" value="5000" />
|
||
</label>
|
||
<button type="button" data-plant-update="battery-total">Update</button>
|
||
</div>
|
||
<div class="plant-setting">
|
||
<div class="plant-current">
|
||
<span>Actual (W)</span>
|
||
<output id="battery-actual-current">0</output>
|
||
</div>
|
||
<label>
|
||
New value
|
||
<input id="battery-actual" type="number" min="0" max="100000" step="1" value="0" />
|
||
</label>
|
||
<button type="button" data-plant-update="battery-actual">Update</button>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<section class="group publish-panel">
|
||
<h3>Publish</h3>
|
||
<div class="meta-row publish-fields">
|
||
<label>
|
||
Publish interval (ms)
|
||
<input id="interval-input" type="number" min="5000" step="500" />
|
||
</label>
|
||
<p id="last-published" class="muted"></p>
|
||
</div>
|
||
<div class="publish-actions">
|
||
<button id="publish-now" type="button">Publish now</button>
|
||
<button id="publish-select-all" type="button">Select all</button>
|
||
<button id="publish-deselect-all" type="button">Deselect all</button>
|
||
</div>
|
||
<p class="muted publish-note">Publish now sends a single telemetry payload. It does not start the repeating loop.</p>
|
||
</section>
|
||
|
||
<div id="telemetry-groups"></div>
|
||
|
||
<details class="rpc-log">
|
||
<summary>Last RPC / shared attributes</summary>
|
||
<pre id="rpc-pre">No RPC yet.</pre>
|
||
</details>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<div id="add-modal" class="overlay hidden">
|
||
<form id="add-form" class="login-card">
|
||
<div id="add-busy" class="busy hidden" aria-live="polite" aria-busy="false">
|
||
<span class="spinner" aria-hidden="true"></span>
|
||
<p id="add-busy-text">Waiting for ThingsBoard…</p>
|
||
</div>
|
||
<p class="eyebrow">New virtual device</p>
|
||
<h2 id="add-title">Add to emulator</h2>
|
||
<p id="add-device-count" class="device-count">0/100 devices</p>
|
||
<p id="add-help" class="muted">Creates a new device in ThingsBoard and this emulator.</p>
|
||
<label class="add-existing-toggle">
|
||
<input id="add-existing" class="device-check" type="checkbox" />
|
||
Add existing device
|
||
</label>
|
||
<div id="add-create-fields">
|
||
<label>
|
||
Name / serial
|
||
<input id="add-name" name="name" placeholder="EMU-AABBCCDDEEFF" />
|
||
</label>
|
||
<label>
|
||
Label
|
||
<input id="add-label" name="label" placeholder="Kitchen emulator" />
|
||
</label>
|
||
<label>
|
||
Type
|
||
<input id="add-type" name="type" placeholder="DVgo" value="DVgo" />
|
||
</label>
|
||
<label>
|
||
Device profile
|
||
<select id="add-profile"></select>
|
||
</label>
|
||
</div>
|
||
<label id="add-token-field" class="hidden">
|
||
Access token
|
||
<input id="add-token" name="accessToken" placeholder="MQTT access token from ThingsBoard" />
|
||
</label>
|
||
<p id="add-error" class="error hidden"></p>
|
||
<div class="row-actions">
|
||
<button type="button" id="add-cancel">Cancel</button>
|
||
<button type="submit" id="add-submit" class="primary">Create device</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div id="confirm-modal" class="overlay hidden">
|
||
<div class="login-card">
|
||
<p class="eyebrow">ThingsBoard timeout</p>
|
||
<h2 id="confirm-title">Save device anyway?</h2>
|
||
<p id="confirm-message" class="muted"></p>
|
||
<div class="row-actions">
|
||
<button type="button" id="confirm-cancel">Don't save</button>
|
||
<button type="button" id="confirm-ok" class="primary">Save to emulator</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="/app.js"></script>
|
||
</body>
|
||
</html>
|