123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425 |
- {% extends "base.html" %}
- {% set active_page = "sensors" %}
- {% block title %} Sensor data {% endblock %}
- {% block divs %}
- {% block breadcrumbs %} {{ super() }} {% endblock %}
-
- <div class="sensor-data charts text-center">
- <div class="row">
- <div class="alert alert-info" id="tzwarn" style="display:none;"></div>
- <div class="alert alert-info" id="dstwarn" style="display:none;"></div>
- <div class="alert alert-info" id="sourcewarn" style="display:none;"></div>
- </div>
- <div class="row on-top-md">
- <div class="col-md-2">
- <div class="header-action-button">
- {% if user_can_delete_sensor %}
- <div>
- <button id="delete-asset-button" class="btn btn-sm btn-responsive btn-danger" onclick="deleteSensor()">
- Delete this sensor
- </button>
- <script>
- $("#delete-asset-button").click(function () {
- if (confirm("Are you sure you want to delete this sensor and all time series data associated with it?")) {
- return true;
- }
- else {
- return false;
- }
- });
- </script>
- </div>
- {% endif %}
- </div>
- <div class="sidepanel-container">
- <div class="left-sidepanel-label">Select dates</div>
- <div class="sidepanel left-sidepanel">
- <div id="datepicker"></div>
- </div>
- </div>
- <div id="chart-type-picker" class="leftside-dropdown dropdown">
- <button class="btn dropdown-toggle" type="button" id="chartTypeDropdown" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- Select chart
- </button>
- <ul class="dropdown-menu center-aligned" aria-labelledby="chartTypeDropdown">
- <li><a class="dropdown-item" href="#" data-chart-type="bar_chart">Bar chart</a></li>
- <li><a class="dropdown-item" href="#" data-chart-type="histogram">Histogram</a></li>
- <li><a class="dropdown-item" href="#" data-chart-type="daily_heatmap">Daily heatmap</a></li>
- <li><a class="dropdown-item" href="#" data-chart-type="weekly_heatmap">Weekly heatmap</a></li>
- </ul>
- </div>
- {% if user_can_update_sensor %}
- <div class="sidepanel-container">
- <div class="left-sidepanel-label">Edit sensor</div>
- <div class="sidepanel left-sidepanel" style="text-align: left;">
- <fieldset>
- <h3>Edit {{ sensor.name }}</h3>
- <small>Parent asset: {{ sensor.generic_asset.name }} (ID: {{ sensor.generic_asset.id }} )</small>
- <form class="form-horizontal" method="POST">
-
- <div class="form-group">
- <label for="name" class="form-label">Name</label>
- <input type="text" class="form-control" id="name" name="name" placeholder="e.g power" value="{{sensor.name}}" required />
- </div>
-
- <button class="btn btn-sm btn-responsive btn-success create-button" type="submit" onclick="updateSensor(event)"
- style="margin-top: 20px; float: right; border: 1px solid var(--light-gray);">
- Save
- </button>
- </form>
- </fieldset>
- </div>
- </div>
- {% endif %}
- </div>
- <div class="col-sm-8">
- <div id="sensorchart" class="card" style="width: 100%;"></div>
- <div id="spinner" hidden="hidden">
- <i class="fa fa-spinner fa-pulse fa-3x fa-fw"></i>
- <span class="sr-only">Loading...</span>
- </div>
- <div class="row">
- <div class="card col-lg-5">
- <h5>Properties</h5>
- <table class="table table-striped">
- <tr>
- <th>Name</th>
- <td>{{ sensor.name }}</td>
- </tr>
- <tr>
- <th>Unit</th>
- <td>{{ sensor.unit }}</td>
- </tr>
- <tr>
- <th>Event resolution</th>
- <td>{{ sensor.event_resolution }}</td>
- </tr>
- <tr>
- <th>Timezone</th>
- <td>{{ sensor.timezone }}</td>
- </tr>
- <tr>
- <th>Knowledge horizon type</th>
- <td>{{ sensor.knowledge_horizon_fnc }}</td>
- </tr>
- </table>
- </div>
- <div class="card col-lg-5" id="statsContainer">
- <span id="spinner-run-simulation" class="spinner-border spinner-border-sm d-none" role="status"></span>
- <h5 id="statsHeader">Statistics</h5>
- <table id="statsTable" class="table table-striped">
- <tbody id="statsTableBody">
- </tbody>
- </table>
- <!-- Dropdown for sourceKey -->
- <div class="dropdown mb-3 d-none" id="sourceKeyDropdownContainer">
- <small class="text-muted">Select source for statistics</small>
- <button class="btn btn-secondary dropdown-toggle" type="button" id="sourceKeyDropdown" data-bs-toggle="dropdown" aria-expanded="false">
- Select Source
- </button>
- <ul class="dropdown-menu" aria-labelledby="sourceKeyDropdown" id="sourceKeyDropdownMenu">
- </ul>
- </div>
- <!-- Alert for no data -->
- <div class="alert alert-warning d-none" id="noDataWarning">
- There is no data for this sensor yet.
- </div>
- <!-- Alert for errors -->
- <div class="alert alert-danger d-none" id="fetchError">
- There was a problem fetching statistics for this sensor's data.
- </div>
- <script>
- document.addEventListener('DOMContentLoaded', function() {
- const spinner = document.getElementById('spinner-run-simulation');
- const tableBody = document.getElementById('statsTableBody');
- const dropdownMenu = document.getElementById('sourceKeyDropdownMenu');
- const dropdownButton = document.getElementById('sourceKeyDropdown');
- const dropdownContainer = document.getElementById('sourceKeyDropdownContainer');
- const propertiesContainer = document.getElementById('propertiesContainer');
- const statsContainer = document.getElementById('statsContainer');
- const noDataWarning = document.getElementById('noDataWarning');
- const fetchError = document.getElementById('fetchError');
- // Show the spinner
- spinner.classList.remove('d-none');
- fetch('/api/v3_0/sensors/' + {{ sensor.id }} + '/stats?sort=false')
- .then(response => response.json())
- .then(data => {
- // Remove 'status' sourceKey
- delete data['status'];
- data = unpackData(data);
- if (Object.keys(data).length > 0) {
- // Show the header and dropdown container
- dropdownContainer.classList.remove('d-none');
- // Populate the dropdown menu with sourceKeys
- Object.keys(data).forEach(sourceKey => {
- const dropdownItem = document.createElement('li');
- const dropdownLink = document.createElement('a');
- dropdownLink.className = 'dropdown-item';
- dropdownLink.href = '#';
- dropdownLink.textContent = sourceKey;
- dropdownLink.dataset.sourceKey = sourceKey;
- dropdownLink.addEventListener('click', function(event) {
- event.preventDefault();
- const selectedSourceKey = event.target.dataset.sourceKey;
- dropdownButton.textContent = selectedSourceKey;
- updateTable(data[selectedSourceKey]);
- });
- dropdownItem.appendChild(dropdownLink);
- dropdownMenu.appendChild(dropdownItem);
- });
- // Update the table with the first sourceKey's data by default
- const firstSourceKey = getLatestBeliefName(data);
- dropdownButton.textContent = firstSourceKey;
- updateTable(data[firstSourceKey]);
- } else {
- // If the stats table is empty, make the properties table full width
- noDataWarning.classList.remove('d-none');
- }
- })
- .catch(error => {
- console.error('Error:', error);
- dropdownContainer.classList.add('d-none');
- fetchError.textContent = 'There was a problem fetching statistics for this sensor\'s data: ' + error.message;
- fetchError.classList.remove('d-none');
- })
- .finally(() => {
- // Hide the spinner
- spinner.classList.add('d-none');
- });
- function updateTable(stats) {
- tableBody.innerHTML = ''; // Clear the table body
- Object.entries(stats).forEach(([key, val]) => {
- const row = document.createElement('tr');
- const keyCell = document.createElement('th');
- const valueCell = document.createElement('td');
- keyCell.textContent = key;
- // Round value to 2 decimal points if it's a number
- if (typeof val === 'number' & key != 'Number of values') {
- valueCell.textContent = val.toFixed(4);
- } else {
- valueCell.textContent = val;
- }
- row.appendChild(keyCell);
- row.appendChild(valueCell);
- tableBody.appendChild(row);
- });
- }
- function getLatestBeliefName(data) {
- return Object.keys(data).reduce((latest, name) => {
- const currentBeliefTime = new Date(data[name]["Last recorded"]);
- const latestBeliefTime = latest ? new Date(data[latest]["Last recorded"]) : new Date(0);
- return currentBeliefTime > latestBeliefTime ? name : latest;
- }, null);
- }
- function unpackData(data) {
- return Object.fromEntries(
- Object.entries(data).map(([key, value]) => {
- if (Array.isArray(value) && value.every(item => Array.isArray(item) && item.length === 2)) {
- return [key, Object.fromEntries(value)];
- }
- console.error(`Invalid entry for key: ${key}`, value);
- return [key, value];
- })
- );
- }
- });
- </script>
- </div>
- </div>
- </div>
- <div class="col-sm-2">
- <div class="replay-container">
- <div id="replay" title="Press 'p' to play/pause/resume or 's' to stop." class="stopped"></div>
- <div id="replay-time"></div>
- </div>
- </div>
- </div>
- <div class="row justify-content-center">
- <div class="col-md-8 offset-md-1">
- <div class="copy-url" title="Click to copy the URL to the current time range to clipboard.">
- <script>
- function toIsoString(date) {
- var tzo = -date.getTimezoneOffset(),
- dif = tzo >= 0 ? '+' : '-',
- pad = function(num) {
- return (num < 10 ? '0' : '') + num;
- };
- return date.getFullYear() +
- '-' + pad(date.getMonth() + 1) +
- '-' + pad(date.getDate()) +
- 'T' + pad(date.getHours()) +
- ':' + pad(date.getMinutes()) +
- ':' + pad(date.getSeconds()) +
- dif + pad(Math.floor(Math.abs(tzo) / 60)) +
- ':' + pad(Math.abs(tzo) % 60);
- }
- $(window).ready(() => {
- picker.on('selected', (startDate, endDate) => {
- startDate = encodeURIComponent(toIsoString(startDate.toJSDate()));
- endDate = encodeURIComponent(toIsoString(endDate.toJSDate()));
- var base_url = window.location.href.split("?")[0];
- var new_url = `${base_url}?start_time=${startDate}&end_time=${endDate}`;
- // change current url without reloading the page
- window.history.pushState({}, null, new_url);
- });
- });
- function copyUrl(event) {
- event.preventDefault();
- if (!window.getSelection) {
- alert('Please copy the URL from the location bar.');
- return;
- }
- const dummy = document.createElement('p');
- var startDate = encodeURIComponent(toIsoString(picker.getStartDate().toJSDate()));
- // add 1 day to end date as datepicker does not include the end date day
- var endDate = picker.getEndDate();
- endDate.setDate(endDate.getDate() + 1);
- endDate = encodeURIComponent(toIsoString(endDate.toJSDate()));
- var base_url = window.location.href.split("?")[0];
- dummy.textContent = `${base_url}?start_time=${startDate}&end_time=${endDate}`
- document.body.appendChild(dummy);
- const range = document.createRange();
- range.setStartBefore(dummy);
- range.setEndAfter(dummy);
- const selection = window.getSelection();
- // First clear, in case the user already selected some other text
- selection.removeAllRanges();
- selection.addRange(range);
- document.execCommand('copy');
- document.body.removeChild(dummy);
- $("#message").show().delay(1000).fadeOut();
- }
- </script>
- <a href="#" onclick="copyUrl(event)" style="display: block; text-align: center;">
- <i class="fa fa-link"></i>
- </a>
- <div id="message" style="display: none; text-align: center;">The URL to the time range currently shown has been copied to your clipboard.</div>
- </div>
- </div>
- </div>
- <hr>
- </div>
- <script src="https://cdnjs.cloudflare.com/ajax/libs/jstimezonedetect/1.0.7/jstz.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/litepicker/dist/litepicker.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/litepicker/dist/plugins/ranges.js"></script>
- <script src="https://cdn.jsdelivr.net/npm/litepicker/dist/plugins/keyboardnav.js"></script>
- <script>
- const apiBasePath = window.location.origin;
- const parentAssetId = "{{sensor.generic_asset.id}}"
- async function updateSensor(event) {
- event.preventDefault();
- const name = document.getElementById("name").value;
- if (!name) {
- showToast("Please fill in all fields.", "error");
- return;
- }
- const data = { name: name }
- const response = await fetch(apiBasePath + "/api/v3_0/sensors/{{ sensor.id }}", {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify(data),
- });
- if (response.ok) {
- const responseData = await response.json();
- const sensorId = responseData.id;
- document.getElementById("name").value = "";
- showToast("Sensor updated successfully.", "success");
- await new Promise(resolve => setTimeout(resolve, 500));
- showToast("Reloading page...", "info");
- await new Promise(resolve => setTimeout(resolve, 2000));
- window.location.reload();
- } else {
- const errorData = await response.json();
- let errorMessage = response.statusText; // Default to statusText
- const messageUnitError = errorData?.message?.json?.unit?.[0];
- if (messageUnitError) {
- errorMessage = messageUnitError;
- } else if (errorData?.message) {
- errorMessage = errorData.message;
- } else if (errorData?.error) {
- errorMessage = errorData.error;
- }
- showToast("Error: " + errorMessage, "error");
- }
- }
- async function deleteSensor() {
- const response = await fetch(apiBasePath + "/api/v3_0/sensors/{{ sensor.id }}", {
- method: "DELETE",
- headers: {
- "Content-Type": "application/json",
- },
- });
- if (response.status === 204) {
- showToast("Sensor deleted successfully.", "success");
- // delay for one second to show toast message
- await new Promise(resolve => setTimeout(resolve, 1000));
- window.location.href = apiBasePath + "/assets/" + parentAssetId;
- } else {
- const errorData = await response.json();
- let errorMessage = response.statusText; // Default to statusText
- const messageUnitError = errorData?.message?.json?.unit?.[0];
- if (messageUnitError) {
- errorMessage = messageUnitError;
- } else if (errorData?.message) {
- errorMessage = errorData.message;
- } else if (errorData?.error) {
- errorMessage = errorData.error;
- }
- showToast("Error: " + errorMessage, "error");
- }
- }
- </script>
- {% block leftsidepanel %} {{ super() }} {% endblock %}
- {% block sensorChartSetup %} {{ super() }} {% endblock %}
- {% endblock %}
|