350 lines
14 KiB
HTML
350 lines
14 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="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">
|
|
<h2>Devices</h2>
|
|
<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 plant</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">
|
|
<h4>Solar</h4>
|
|
<div class="plant-setting">
|
|
<span class="lamp" data-power-lamp="solar" aria-hidden="true"></span>
|
|
<div class="plant-current">
|
|
<span>Current value</span>
|
|
<output data-power-value="solar">0 W</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">
|
|
<h4>Grid</h4>
|
|
<div class="plant-setting">
|
|
<span class="lamp" data-power-lamp="grid" aria-hidden="true"></span>
|
|
<div class="plant-current">
|
|
<span>Current value</span>
|
|
<output data-power-value="grid">0 W</output>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
<section class="plant-card">
|
|
<h4>Home load</h4>
|
|
<div class="plant-setting">
|
|
<span class="lamp" data-power-lamp="home" aria-hidden="true"></span>
|
|
<div class="plant-current">
|
|
<span>Current value</span>
|
|
<output data-power-value="home">0 W</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">
|
|
<h4>Hot water</h4>
|
|
<div class="lamps">
|
|
<div class="lamp-card">
|
|
<span id="hw-lamp" class="lamp" aria-hidden="true"></span>
|
|
<div>
|
|
<strong>Hot water boost</strong>
|
|
<small id="hw-lamp-detail">Off</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="plant-setting">
|
|
<div class="plant-current">
|
|
<span>Current value (°C)</span>
|
|
<output id="tank-temp">—</output>
|
|
</div>
|
|
</div>
|
|
<div class="plant-setting">
|
|
<div class="plant-current">
|
|
<span>Current value (°C / min)</span>
|
|
<output id="heat-rate-current">1</output>
|
|
</div>
|
|
<label>
|
|
Heat rate (°C / min)
|
|
<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>Current value (°C)</span>
|
|
<output id="thermostat-current">65</output>
|
|
</div>
|
|
<label>
|
|
Thermostat max (°C)
|
|
<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">
|
|
<span class="lamp" data-power-lamp="hotWater" aria-hidden="true"></span>
|
|
<div class="plant-current">
|
|
<span>Current value</span>
|
|
<output data-power-value="hotWater">0 W</output>
|
|
</div>
|
|
<label>
|
|
New value (W)
|
|
<input id="power-hot-water" data-power-input="hotWater" type="number" min="0" max="100000" step="1" value="0" />
|
|
</label>
|
|
<button type="button" data-power-update="hotWater">Update</button>
|
|
</div>
|
|
</section>
|
|
<section class="plant-card">
|
|
<h4>Aux</h4>
|
|
<div class="lamps">
|
|
<div class="lamp-card">
|
|
<span id="aux-lamp" class="lamp" aria-hidden="true"></span>
|
|
<div>
|
|
<strong>Aux boost / divert</strong>
|
|
<small id="aux-lamp-detail">Off</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="plant-setting">
|
|
<span class="lamp" data-power-lamp="aux" aria-hidden="true"></span>
|
|
<div class="plant-current">
|
|
<span>Current value</span>
|
|
<output data-power-value="aux">0 W</output>
|
|
</div>
|
|
<label>
|
|
New value (W)
|
|
<input id="power-aux" data-power-input="aux" type="number" min="0" max="100000" step="1" value="0" />
|
|
</label>
|
|
<button type="button" data-power-update="aux">Update</button>
|
|
</div>
|
|
</section>
|
|
<section class="plant-card">
|
|
<h4>Battery</h4>
|
|
<div class="lamps">
|
|
<div class="lamp-card">
|
|
<span id="batt-lamp" class="lamp" aria-hidden="true"></span>
|
|
<div>
|
|
<strong>Battery Charging</strong>
|
|
<small id="batt-lamp-detail">Off</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="plant-setting">
|
|
<div class="plant-current">
|
|
<span>Current value (% / min)</span>
|
|
<output id="charge-rate-current">10</output>
|
|
</div>
|
|
<label>
|
|
Charge rate (% / min)
|
|
<input id="charge-rate" type="number" min="0" max="100" step="1" />
|
|
</label>
|
|
<button type="button" data-plant-update="charge">Update</button>
|
|
</div>
|
|
<div class="plant-setting">
|
|
<span class="lamp" data-power-lamp="battery" aria-hidden="true"></span>
|
|
<div class="plant-current">
|
|
<span>Current value</span>
|
|
<output data-power-value="battery">0 W</output>
|
|
</div>
|
|
<label>
|
|
Direction
|
|
<select id="power-battery-mode" data-power-mode="battery" aria-label="Battery direction">
|
|
<option value="draw">Draw</option>
|
|
<option value="supply">Supply</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
New value (W)
|
|
<input id="power-battery" data-power-input="battery" type="number" min="0" max="100000" step="1" value="0" />
|
|
</label>
|
|
<button type="button" data-power-update="battery">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="500" 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>
|
|
</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-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>
|