asset_context.html 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361
  1. {% extends "base.html" %}
  2. {% set active_page = "assets" %}
  3. {% block title %} {{ asset.name }} - Scenario {% endblock %}
  4. {% block divs %}
  5. {% block breadcrumbs %} {{ super() }} {% endblock %}
  6. <div class="container-fluid">
  7. <div class="row">
  8. <div class="col-sm-2"></div>
  9. <div class="col-sm-8 card">
  10. <div class="row">
  11. <div class="col-sm-9">
  12. <div class="d-flex justify-content-between align-items-center" style="margin-bottom: 10px;">
  13. <h1>
  14. Asset: {{ asset.name }}
  15. </h1>
  16. <div class="d-flex">
  17. <button class="btn btn-sm btn-primary me-2" onclick='openSensorsModal()'>
  18. Show sensors
  19. </button>
  20. <button class="btn btn-sm btn-primary me-2" data-bs-toggle="modal"
  21. data-bs-target="#flexContextModal">
  22. Edit flex-context
  23. </button>
  24. </div>
  25. </div>
  26. <p>Type: {{ asset.generic_asset_type.name.split('.')[-1] | title }}</p>
  27. </div>
  28. <div class="col-sm-3">
  29. {% if can_delete %}
  30. <button type="button" class="btn delete-button" onClick="delete_asset(event, {{ asset.id }}, '{{ asset.name }}')">Delete Scenario</button>
  31. {% endif %}
  32. <!-- Tabs for toggling content -->
  33. <div class="tabs-container">
  34. <ul class="nav nav-tabs">
  35. <!-- Reorder the tabs based on the length of the assets -->
  36. {% if assets|length > 2 %}
  37. <!-- Structure tab will be first if assets length > 2 -->
  38. <li class="nav-item">
  39. <a class="nav-link active" id="structure-tab" data-bs-toggle="tab" href="#structure">Structure</a>
  40. </li>
  41. <li class="nav-item">
  42. <a class="nav-link" id="location-tab" data-bs-toggle="tab" href="#location">Location</a>
  43. </li>
  44. {% else %}
  45. <!-- Location tab will be first if assets length <= 2 -->
  46. <li class="nav-item">
  47. <a class="nav-link active" id="location-tab" data-bs-toggle="tab" href="#location">Location</a>
  48. </li>
  49. <li class="nav-item">
  50. <a class="nav-link" id="structure-tab" data-bs-toggle="tab" href="#structure">Structure</a>
  51. </li>
  52. {% endif %}
  53. </ul>
  54. </div>
  55. </div>
  56. </div>
  57. <!-- Tab Content (outside col-sm) -->
  58. <div class="tab-content">
  59. <!-- Structure Content -->
  60. <div class="tab-pane fade {% if assets|length > 2 %}show active{% endif %}" id="structure">
  61. <div class="col-sm-12">
  62. {% from "_macros.html" import show_tree %}
  63. {{ show_tree(assets, asset.name) }}
  64. </div>
  65. </div>
  66. <!-- Location Content -->
  67. <div class="tab-pane fade {% if assets|length <= 2 %}show active{% endif %}" id="location">
  68. <!-- Location-related content here -->
  69. <div id="mapid" style="height: 400px;"></div>
  70. </div>
  71. </div>
  72. </div>
  73. <div class="col-sm-2"></div>
  74. <!-- Bootstrap Modal for Sensors -->
  75. <div class="modal fade" id="sensorsModal" tabindex="-1" aria-labelledby="modalTitle" aria-hidden="true">
  76. <div class="modal-dialog modal-lg">
  77. <div class="modal-content">
  78. <div class="modal-header">
  79. <h5 class="modal-title" id="modalTitle">Node Details</h5>
  80. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  81. </div>
  82. <div class="modal-body">
  83. <table id="modalTable" class="table table-bordered">
  84. <thead id="modalTableHead"></thead>
  85. <tbody id="modalTableBody"></tbody>
  86. </table>
  87. </div>
  88. <div class="modal-footer">
  89. <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. <!-- Flex Context Modal -->
  96. <div class="modal fade modal-xl" id="flexContextModal" tabindex="-1" aria-labelledby="flexContextModalLabel"
  97. aria-hidden="true">
  98. <div class="modal-dialog">
  99. <div class="modal-content">
  100. <div class="modal-header">
  101. <h5 class="modal-title pe-2">Edit Asset's FlexContext</h5>
  102. <div class="dropdown" data-bs-auto-close="outside">
  103. <span class="fa fa-info dropdown-toggle" role="button" data-bs-toggle="dropdown"
  104. aria-expanded="false" rel="tooltip" aria-hidden="true" tabindex="0"
  105. data-bs-placement="right" data-bs-toggle="tooltip"></span>
  106. <div class="dropdown-menu p-3" style="width: 400px;">
  107. <div>
  108. <strong>Help</strong>
  109. <p class="mb-2 pt-2">
  110. Here, you can edit the flexibility context for this asset. This describes information about the managed system as a
  111. whole, in order to assess the value of activating flexibility. You can read more <a href="https://flexmeasures.readthedocs.io/stable/features/scheduling.html#the-flex-context">in the documentation</a>.
  112. <br/>
  113. <br/>
  114. These fields can also be sent via the API to FlexMeasures, but setting them here permanently might be more convenient.
  115. Some fields can point to sensors, so they will always represent the dynamics of the asset's environment (as long as that sensor has current data).
  116. <br/>
  117. <br/>
  118. To add a field, choose it from the dropdown menu in the top right corner, then click the "Add Field" button'.
  119. Set a value in the right panel, using the provided form to set the field's value.
  120. </p>
  121. </div>
  122. </div>
  123. </div>
  124. <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
  125. </div>
  126. <div class="modal-body">
  127. <div class="row">
  128. <!-- Left Column -->
  129. <div class="col-5">
  130. <form id="flexContextForm">
  131. </form>
  132. </div>
  133. <!-- Right Column -->
  134. <div class="col-7">
  135. <div>
  136. <!-- Loading Spinner -->
  137. <div id="spinnerElement" class="d-flex justify-content-center align-items-center mb-2"
  138. style="height: 50vh; display: none !important;">
  139. <div class="spinner-border text-primary" role="status"
  140. style="width: 4rem; height: 4rem;">
  141. <span class="visually-hidden">Loading...</span>
  142. </div>
  143. </div>
  144. <!-- Settings -->
  145. <div>
  146. <div class="row">
  147. <div class="col-10">
  148. <select class="form-select" id="flexSelect">
  149. <option value="blank">Select flex-context field to add</option>
  150. </select>
  151. </div>
  152. <div class="col-2 p-0">
  153. <button class="btn btn-secondary btn-md"
  154. onclick="addFlexContextField()">Add Field</button>
  155. </div>
  156. </div>
  157. <div id="flexInfoContainer" class="mt-3"></div>
  158. </div>
  159. <hr class="border border-dark" />
  160. <div id="flexOptionsContainer" class="mt-3"></div>
  161. <div id="sensorsSearchResults" class="row mt-3" style="display: none;"></div>
  162. </div>
  163. </div>
  164. </div>
  165. </div>
  166. </div>
  167. </div>
  168. </div>
  169. </div>
  170. <script>
  171. function openSensorsModal() {
  172. const sensors = {{ current_asset_sensors | safe }};
  173. document.getElementById("modalTitle").innerText = "Sensors for " + '{{ asset.name }}';
  174. let tableHead = document.getElementById("modalTableHead");
  175. let tableBody = document.getElementById("modalTableBody");
  176. // Clear previous content
  177. tableHead.innerHTML = "";
  178. tableBody.innerHTML = "";
  179. if (sensors && sensors.length > 0) {
  180. let keys = Object.keys(sensors[0]); // e.g., ['name', 'unit']
  181. keys.pop("link"); // Remove the 'link' from the table
  182. // Create table header row
  183. let headerRow = document.createElement("tr");
  184. keys.forEach(key => {
  185. let headerText = key.charAt(0).toUpperCase() + key.slice(1);
  186. let th = document.createElement("th"); // Use <th> for header cells
  187. th.innerText = headerText;
  188. th.setAttribute("aria-label", headerText); // set aria-label so floatThead can pick it up
  189. headerRow.appendChild(th);
  190. });
  191. tableHead.appendChild(headerRow);
  192. // Force destroy the floatThead instance
  193. setTimeout(function(){
  194. if ($.fn.floatThead) {
  195. $('#modalTable').floatThead('destroy');
  196. }
  197. }, 200);
  198. // Populate table with sensor data
  199. sensors.forEach(sensor => {
  200. let row = document.createElement("tr");
  201. keys.forEach(key => {
  202. let td = document.createElement("td");
  203. if (key === "name") {
  204. // Create an anchor element
  205. let a = document.createElement("a");
  206. a.innerText = sensor[key] !== undefined ? sensor[key] : "N/A";
  207. // Set the href; you can modify this as needed, for example using sensor.link if available
  208. a.href = sensor.link;
  209. td.appendChild(a);
  210. } else {
  211. td.innerText = sensor[key] !== undefined ? sensor[key] : "N/A";
  212. }
  213. row.appendChild(td);
  214. });
  215. tableBody.appendChild(row);
  216. });
  217. } else {
  218. tableBody.innerHTML = "<tr><td colspan='100%'>No sensor data available</td></tr>";
  219. }
  220. // Show Bootstrap modal
  221. let modal = new bootstrap.Modal(document.getElementById('sensorsModal'));
  222. modal.show();
  223. }
  224. </script>
  225. <style>
  226. details {
  227. display: none;
  228. }
  229. </style>
  230. <!-- Initialise the map -->
  231. <script src="https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet-src.min.js"></script>
  232. <script src="{{ url_for('flexmeasures_ui.static', filename='js/map-init.js') }}"></script>
  233. <script type="text/javascript">
  234. // create map
  235. var assetMap = L
  236. .map('mapid', { center: [{{ asset.latitude | replace("None", 10) }}, {{ asset.longitude | replace("None", 10) }}], zoom: 10})
  237. .on('popupopen', function () {
  238. $(function () {
  239. $('[data-toggle="tooltip"]').tooltip();
  240. });
  241. });
  242. addTileLayer(assetMap, '{{ mapboxAccessToken }}');
  243. // create marker
  244. var asset_icon = new L.DivIcon({
  245. className: 'map-icon',
  246. html: '<i class="icon-empty-marker center-icon supersize"></i><i class="overlay center-icon {{ asset.generic_asset_type.name | default("info") | asset_icon }}"></i>',
  247. iconSize: [100, 100], // size of the icon
  248. iconAnchor: [50, 50], // point of the icon which will correspond to marker's location
  249. popupAnchor: [0, -50] // point from which the popup should open relative to the iconAnchor
  250. });
  251. var marker = L
  252. .marker(
  253. [{{ asset.latitude | replace("None", 10) }}, {{ asset.longitude | replace("None", 10) }}],
  254. { icon: asset_icon }
  255. ).addTo(assetMap);
  256. </script>
  257. <script>
  258. const senSearchResEle = document.getElementById("sensorsSearchResults")
  259. const flexContextFormEle = document.getElementById("flexContextForm")
  260. const flexContexFormModal = document.getElementById('flexContextModal');
  261. const flexSelect = document.getElementById('flexSelect');
  262. const flexInfoContainer = document.getElementById('flexInfoContainer');
  263. const flexOptionsContainer = document.getElementById('flexOptionsContainer');
  264. let availableUnitsRawJSON = "{{ available_units | safe }}";
  265. availableUnitsRawJSON = availableUnitsRawJSON.replace(/'/g, '"');
  266. const availableUnits = JSON.parse(availableUnitsRawJSON);
  267. let assetFlexContext = {
  268. "consumption-price": null,
  269. "production-price": null,
  270. "site-power-capacity": null,
  271. "site-production-capacity": null,
  272. "site-consumption-capacity": null,
  273. "site-consumption-breach-price": null,
  274. "site-production-breach-price": null,
  275. "site-peak-consumption": null,
  276. "site-peak-consumption-price": null,
  277. "site-peak-production": null,
  278. "site-peak-production-price": null,
  279. "inflexible-device-sensors": []
  280. }
  281. let assetFlexContextRawJSON = "{{ asset.flex_context | safe }}";
  282. assetFlexContextRawJSON = processAssetRawFlexContextJSON(assetFlexContextRawJSON);
  283. const apiSensorsListElement = document.getElementById("apiSensorsList");
  284. const spinnerElement = document.getElementById('spinnerElement');
  285. let sensorsToShowRawJSON = "{{ asset.sensors_to_show | safe }}";
  286. sensorsToShowRawJSON = sensorsToShowRawJSON.replace(/'/g, '"');
  287. let sensorsToShow = JSON.parse(sensorsToShowRawJSON);
  288. let cachedFilteredSensors = []; // keeps track of the filtered sensors
  289. let editingIndex; // keeps track of which graph we are currently editing
  290. let savedGraphIndex; // keeps track of the graph that is currently selected
  291. let selectedGraphTitle; // keeps track of the graph title that is currently selected
  292. const apiBasePath = window.location.origin;
  293. const sensorsApiUrl = `${apiBasePath}/api/v3_0/sensors?page=1&per_page=100&asset_id={{ asset.id }}`;
  294. function processAssetRawFlexContextJSON(rawJSON){
  295. let processedJSON = rawJSON.replace(/'/g, '"');
  296. // change None to null
  297. processedJSON = processedJSON.replace(/None/g, 'null');
  298. // update the assetFlexContext fields
  299. processedJSON = JSON.parse(processedJSON);
  300. for (const [key, value] of Object.entries(processedJSON)) {
  301. if (key in assetFlexContext) {
  302. assetFlexContext[key] = processedJSON[key];
  303. } else {
  304. assetFlexContext[key] = null;
  305. }
  306. }
  307. return processedJSON;
  308. }
  309. // Fetch Account Details
  310. async function getAccount(accountId) {
  311. const cacheKey = `account_${accountId}`;
  312. const cachedData = localStorage.getItem(cacheKey);
  313. if (cachedData) {
  314. return JSON.parse(cachedData);
  315. }
  316. const apiUrl = apiBasePath + "/api/v3_0/accounts/" + accountId;
  317. const response = await fetch(apiUrl);
  318. const account = await response.json();
  319. localStorage.setItem(cacheKey, JSON.stringify(account));
  320. return account;
  321. }
  322. // Fetch Asset Details
  323. async function getAsset(assetId, useCache = true) {
  324. const cacheKey = `asset_${assetId}`;
  325. const cachedData = localStorage.getItem(cacheKey);
  326. if (cachedData && useCache) {
  327. return JSON.parse(cachedData);
  328. }
  329. const apiUrl = apiBasePath + "/api/v3_0/assets/" + assetId;
  330. const response = await fetch(apiUrl);
  331. const asset = await response.json();
  332. localStorage.setItem(cacheKey, JSON.stringify(asset));
  333. return asset;
  334. }
  335. // Fetch Sensor Details
  336. async function getSensor(id) {
  337. const cacheKey = `sensor_${id}`;
  338. const cachedData = localStorage.getItem(cacheKey);
  339. if (cachedData) {
  340. return JSON.parse(cachedData);
  341. }
  342. const apiUrl = apiBasePath + "/api/v3_0/sensors/" + id;
  343. const response = await fetch(apiUrl);
  344. const sensor = await response.json();
  345. localStorage.setItem(cacheKey, JSON.stringify(sensor));
  346. return sensor;
  347. }
  348. // highlight selected graph
  349. async function selectGraph(graphIndex) {
  350. if (graphIndex !== undefined) {
  351. savedGraphIndex = graphIndex;
  352. selectedGraphTitle = sensorsToShow[graphIndex]?.title;
  353. // check if graphIndex still exists(This is because this function is called when the removed button is clicked as well)
  354. if (sensorsToShow[graphIndex]) {
  355. renderApiSensors(cachedFilteredSensors, graphIndex);
  356. } else {
  357. renderApiSensors(cachedFilteredSensors);
  358. }
  359. } else {
  360. savedGraphIndex = undefined;
  361. selectedGraphTitle = undefined;
  362. renderGraphCards();
  363. }
  364. }
  365. // Function to render the graph cards
  366. async function renderGraphCards() {
  367. const graphList = document.getElementById("graphList");
  368. graphList.innerHTML = "";
  369. const newSensorsToShow = [];
  370. if (sensorsToShow.length === 0) {
  371. return;
  372. }
  373. for (const [index, item] of sensorsToShow.entries()) {
  374. const col = document.createElement("div");
  375. col.classList.add("col-12", "mb-1");
  376. const sensorsUnits = [];
  377. // the initializing of the newItem is to handle the case where the item is an array of ID's instead of an object
  378. const newItem = {
  379. title: item.title ?? "No Title",
  380. sensors: item.sensors ?? (Array.isArray(item) ? item : [item]),
  381. }
  382. newSensorsToShow.push(newItem);
  383. const sensorsContent = await Promise.all(
  384. newItem.sensors.map(async (sensor, sensorIndex) => {
  385. const sensorData = await getSensor(sensor);
  386. const Asset = await getAsset(sensorData.generic_asset_id);
  387. const Account = await getAccount(Asset.account_id);
  388. sensorsUnits.push(sensorData.unit);
  389. return `
  390. <div class="p-1 mb-3 border-bottom border-secondary">
  391. <div class="d-flex justify-content-between">
  392. <div>
  393. <b>ID:</b> <a href="${apiBasePath}/sensors/${sensorData.id}">${sensorData.id}</a>,
  394. <b>Unit:</b> ${sensorData.unit},
  395. <b>Name:</b> ${sensorData.name},
  396. <div style="padding-top: 1px;"></div>
  397. <b>Asset:</b> ${Asset.name},
  398. <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
  399. </div>
  400. <i class="fa fa-times"
  401. onclick="removeSensorFromGraph(${index}, ${sensorIndex})"
  402. data-bs-toggle="tooltip"
  403. data-bs-placement="top"
  404. title="Remove Sensor"
  405. style="cursor: pointer;"
  406. ></i>
  407. </div>
  408. </div>`;
  409. }));
  410. const uniqueUnits = [...new Set(sensorsUnits)];
  411. col.innerHTML = `
  412. <div class="card m-0 p-1">
  413. <div class="card-body card-highlight ${newItem.title === selectedGraphTitle ? " border-on-click" : ""}" id="graph_${index}" onclick="selectGraph(${index})">
  414. <div class="d-flex align-items-center">
  415. <div>
  416. ${editingIndex === index
  417. ? `<input type="text" class="form-control mb-2 me-2" id="editTitle_${index}" value="${newItem.title}" onkeydown="handleEnterKeyEventForTitleEditing(event, ${index})" />`
  418. : `<h5 class="card-title me-2">${newItem.title}</h5>`
  419. }
  420. </div>
  421. <div>
  422. ${editingIndex === index
  423. ? `<button class="btn btn-success btn-sm ms-2" id="saveTitleBtn" onclick="saveGraphTitle(${index})">Save</button>`
  424. : `<button class="btn btn-warning btn-sm ms-2" id="editTitleBtn" onclick="editGraphTitle(${index})">Edit</button>`
  425. }
  426. </div>
  427. </div>
  428. <h5 class="card-title pt-2"><b> Sensors: </b></h5>
  429. <div>
  430. ${sensorsContent.length > 0 ? sensorsContent.join("") : `<div class="alert alert-warning" role="alert"> No sensors added to this graph. Add sensors by selecting them from the right</div>`}
  431. ${uniqueUnits.length > 1 ? `<div class="alert alert-warning" role="alert">Note that you are showing sensors with different units</div>` : ""}
  432. </div>
  433. <button class="btn btn-danger btn-sm me-2" onclick="(function(e) { e.stopPropagation(); removeGraph(${index}); })(event)">Remove</button>
  434. <button class="btn btn-secondary btn-sm me-2" onclick="(function(e) { e.stopPropagation(); moveGraphUp(${index}); })(event)" ${index === 0 ? "disabled" : ""}>Move Up</button>
  435. <button class="btn btn-secondary btn-sm" onclick="(function(e) { e.stopPropagation(); moveGraphDown(${index}); })(event)" ${index === sensorsToShow.length - 1 ? "disabled" : ""}>Move Down</button>
  436. </div>
  437. </div>`;
  438. graphList.appendChild(col);
  439. }
  440. sensorsToShow = newSensorsToShow;
  441. }
  442. /* This search function is tied to the sensorsToShow Form */
  443. async function searchSensorsGraphForm() {
  444. const searchValue = document.getElementById("searchInput").value.toLowerCase();
  445. const filterValue = document.getElementById("unitsSelect").value;
  446. const highlightedCard = document.querySelector('.border-on-click');
  447. spinnerElement.style.display = 'flex';
  448. apiSensorsListElement.style.display = 'none';
  449. // Due to the nature of async functions, the highlightedCard might not be available
  450. // when the filterSensors function is called. So, we need to check if it exists
  451. if (highlightedCard) {
  452. const cardId = highlightedCard.id;
  453. const index = cardId.split("_")[1];
  454. savedGraphIndex = index;
  455. selectedGraphTitle = sensorsToShow[index].title;
  456. } else {
  457. savedGraphIndex = undefined;
  458. selectedGraphTitle = undefined;
  459. }
  460. // check if apiSensorsList has been rendered
  461. if (apiSensorsListElement.innerHTML === "") {
  462. document.getElementById('apiSensorsList').style.display = 'block';
  463. }
  464. const params = new URLSearchParams();
  465. if (searchValue) {
  466. params.append('filter', searchValue);
  467. }
  468. if (filterValue !== "null") {
  469. params.append('unit', filterValue);
  470. }
  471. const apiUrl = `${sensorsApiUrl}&${params.toString()} `;
  472. try {
  473. const response = await fetch(apiUrl);
  474. if (!response.ok) {
  475. throw new Error('Failed to fetch sensors');
  476. }
  477. const responseData = await response.json();
  478. const filteredSensors = responseData.data;
  479. // Render the fetched sensors
  480. if (savedGraphIndex !== null && savedGraphIndex !== undefined) {
  481. renderApiSensors(filteredSensors, savedGraphIndex);
  482. } else {
  483. renderApiSensors(filteredSensors);
  484. }
  485. cachedFilteredSensors = filteredSensors;
  486. spinnerElement.classList.add('hidden-important');
  487. apiSensorsListElement.style.display = 'block';
  488. } catch (error) {
  489. const errorMessage = error?.message || error?.error || response.statusText;
  490. showToast(`Failed to search sensors: ${errorMessage}`, "error");
  491. }
  492. }
  493. /* This search function is tied to the FlexContext Form */
  494. async function searchSensorsFlexContextForm() {
  495. const searchValue = document.getElementById("flexContextSensorSearch").value.toLowerCase();
  496. spinnerElement.style.display = 'flex';
  497. senSearchResEle.style.display = 'none';
  498. const flexUnitsSelectValue = document.getElementById("flexUnitsSelect").value;
  499. const flexCheckBox = document.getElementById('flexCheckBox');
  500. const params = new URLSearchParams();
  501. if (searchValue) {
  502. params.append('filter', searchValue);
  503. }
  504. if (flexCheckBox.checked) {
  505. params.append('include_public_assets', true);
  506. }
  507. if(flexUnitsSelectValue != "null"){
  508. params.append('unit', flexUnitsSelectValue);
  509. }
  510. const apiUrl = `${sensorsApiUrl}&${params.toString()}`;
  511. try {
  512. const response = await fetch(apiUrl);
  513. if (!response.ok) {
  514. throw new Error('Failed to fetch sensors');
  515. }
  516. const responseData = await response.json();
  517. const filteredSensors = responseData.data;
  518. // Render the fetched sensors
  519. renderSensorSearchResults(filteredSensors);
  520. spinnerElement.classList.add('hidden-important');
  521. senSearchResEle.style.display = 'block';
  522. } catch (error) {
  523. const errorMessage = error?.message || error?.error || response.statusText;
  524. showToast(`Failed to search sensors: ${errorMessage}`, "error");
  525. }
  526. }
  527. // ============== Graph Cards Management ============== //
  528. async function removeGraph(index) {
  529. sensorsToShow.splice(index, 1);
  530. savedGraphIndex = undefined;
  531. selectedGraphTitle = undefined;
  532. editingIndex = undefined;
  533. renderGraphCards();
  534. renderApiSensors(cachedFilteredSensors);
  535. }
  536. async function swapItems(index1, index2) {
  537. if (index1 >= 0 && index2 >= 0 && index1 < sensorsToShow.length && index2 < sensorsToShow.length) {
  538. [sensorsToShow[index1], sensorsToShow[index2]] = [sensorsToShow[index2], sensorsToShow[index1]];
  539. renderGraphCards()
  540. renderApiSensors(cachedFilteredSensors, index2);
  541. }
  542. }
  543. async function moveGraphUp(index) {
  544. if (index > 0) {
  545. await swapItems(index, index - 1);
  546. }
  547. }
  548. async function moveGraphDown(index) {
  549. if (index < sensorsToShow.length - 1) {
  550. await swapItems(index, index + 1);
  551. }
  552. }
  553. function editGraphTitle(index) {
  554. editingIndex = index;
  555. }
  556. async function saveGraphTitle(index) {
  557. const newTitle = document.getElementById(`editTitle_${index}`).value;
  558. sensorsToShow[index].title = newTitle;
  559. editingIndex = null;
  560. selectedGraphTitle = newTitle;
  561. renderGraphCards();
  562. }
  563. // ============== Graph Cards Management ============== //
  564. // Render the available API sensors
  565. function renderApiSensors(sensors, graphIndex) {
  566. // graphIndex is undefined when the sensors are being added to the graph
  567. // graphIndex is defined when the sensors are being added to the graph cards. In other words
  568. // when more sensors are being added to single graph
  569. apiSensorsList.innerHTML = ""; // Clear the previous sensors
  570. if (sensors.length === 0) {
  571. apiSensorsList.innerHTML = "<h3>No sensors found</h3>";
  572. return;
  573. }
  574. sensors.forEach(async (sensor) => {
  575. const Asset = await getAsset(sensor.generic_asset_id);
  576. const Account = await getAccount(Asset.account_id);
  577. const col = document.createElement("div");
  578. col.classList.add("col-12", "mb-1");
  579. col.innerHTML = `
  580. <div class="card m-0">
  581. <div class="card-body p-0 sensor-card">
  582. <h5 class="card-title">${sensor.name}</h5>
  583. <p class="card-text">
  584. <b>ID:</b> <a href="${apiBasePath}/sensors/${sensor.id}">${sensor.id}</a>,
  585. <b>Unit:</b> ${sensor.unit},
  586. <b>Asset:</b> ${Asset.name},
  587. <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
  588. </p>
  589. ${graphIndex !== undefined && savedGraphIndex !== undefined && selectedGraphTitle !== undefined
  590. ? `<button class="btn btn-primary btn-sm" onclick="addSensorToExistingGraph(${graphIndex}, ${sensor.id})">Add to '${selectedGraphTitle}' Graph</button>`
  591. : `<button class="btn btn-primary btn-sm" onclick="addSensorAsGraph(${sensor.id})">Add new Graph</button>`
  592. }
  593. </div>
  594. </div>
  595. `;
  596. apiSensorsList.appendChild(col);
  597. });
  598. }
  599. function renderSensorSearchResults(sensors) {
  600. senSearchResEle.innerHTML = "";
  601. if (sensors.length === 0) {
  602. senSearchResEle.innerHTML = "<h4>No sensors found</h4>";
  603. return;
  604. }
  605. sensors.forEach(async (sensor) => {
  606. const Asset = await getAsset(sensor.generic_asset_id);
  607. const Account = await getAccount(Asset.account_id);
  608. const col = document.createElement("div");
  609. col.classList.add("col-12", "mb-1");
  610. col.innerHTML = `
  611. <div class="card m-0">
  612. <div class="card-body p-0 sensor-card">
  613. <h5 class="card-title">${sensor.name}</h5>
  614. <p class="card-text">
  615. <b>ID:</b> <a href="${apiBasePath}/sensors/${sensor.id}">${sensor.id}</a>,
  616. <b>Unit:</b> ${sensor.unit},
  617. <b>Asset:</b> ${Asset.name},
  618. <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
  619. </p>
  620. <button class="btn btn-primary btn-sm" onclick="udpateFlexContextFieldValue('sensor', ${sensor.id})">Add Sensor</button>
  621. </div>
  622. </div>
  623. `;
  624. senSearchResEle.appendChild(col);
  625. });
  626. }
  627. async function updateSensorToShow() {
  628. const apiURL = apiBasePath + "/api/v3_0/assets/{{ asset.id }}";
  629. const requestBody = JSON.stringify({ sensors_to_show: JSON.stringify(sensorsToShow) });
  630. // Show spinner
  631. document.getElementById('spinner').style.display = 'block';
  632. const response = await fetch(apiURL, {
  633. method: "PATCH",
  634. headers: {
  635. "Content-Type": "application/json",
  636. },
  637. body: requestBody,
  638. });
  639. if (!response.ok) {
  640. const errorData = await response.json();
  641. const errorMessage = errorData?.message || errorData?.error || response.statusText;
  642. showToast(`Failed to update sensors to show: ${errorMessage}`, "error");
  643. } else {
  644. showToast("Sensor graphs updated successfully", "success");
  645. }
  646. }
  647. async function updateFlexContext() {
  648. const apiURL = apiBasePath + "/api/v3_0/assets/{{ asset.id }}";
  649. // Clean null values
  650. for (const [key, value] of Object.entries(assetFlexContext)) {
  651. if (value === null) {
  652. delete assetFlexContext[key];
  653. }
  654. }
  655. const requestBody = JSON.stringify({ flex_context: JSON.stringify(assetFlexContext) });
  656. const response = await fetch(apiURL, {
  657. method: "PATCH",
  658. headers: {
  659. "Content-Type": "application/json",
  660. },
  661. body: requestBody,
  662. });
  663. if (!response.ok) {
  664. const errorData = await response.json();
  665. const errorMessage = errorData?.message || errorData?.error || response.statusText;
  666. showToast(`Failed to update the flex context: ${errorMessage}`, "error");
  667. // Revert to previous state
  668. const asset = await getAsset("{{ asset.id }}", false);
  669. const previousFlexContext = await processAssetRawFlexContextJSON(asset.flex_context);
  670. assetFlexContext = previousFlexContext;
  671. renderFlexContextForm();
  672. } else {
  673. showToast("Flex context updated successfully", "success");
  674. }
  675. }
  676. // Add a sensor as a new graph card
  677. function addSensorAsGraph(id) {
  678. const newAsset = {
  679. title: "No Title",
  680. sensors: [id],
  681. };
  682. sensorsToShow.push(newAsset);
  683. renderGraphCards();
  684. }
  685. // Add blank graph to the graph cards
  686. function addNewGraph() {
  687. const newAsset = {
  688. title: "No Title " + (sensorsToShow.length + 1),
  689. sensors: [],
  690. };
  691. selectedGraphTitle = newAsset.title;
  692. sensorsToShow.push(newAsset);
  693. selectGraph(sensorsToShow.length - 1)
  694. renderGraphCards();
  695. }
  696. // Add Sensor to an existing graph card
  697. function addSensorToExistingGraph(graphIndex, sensorId) {
  698. sensorsToShow[graphIndex].sensors.push(sensorId);
  699. renderGraphCards();
  700. }
  701. // Remove sensor from the graph sensor list
  702. function removeSensorFromGraph(graphIndex, sensorIndex) {
  703. sensorsToShow[graphIndex].sensors.splice(sensorIndex, 1);
  704. renderGraphCards();
  705. renderApiSensors(cachedFilteredSensors);
  706. }
  707. function handleEnterKeyEventForTitleEditing(event, graphIndex) {
  708. if (event.key === "Enter") {
  709. saveGraphTitle(graphIndex);
  710. renderApiSensors(cachedFilteredSensors, graphIndex);
  711. }
  712. }
  713. // ============== Flex Context Management BEGIN ============== //
  714. async function removeFlexContextField(fieldName) {
  715. assetFlexContext[fieldName] = null;
  716. renderFlexContextForm();
  717. }
  718. async function removeFlexContextSensorValue(fieldName, sensorId) {
  719. if (sensorId) {
  720. const fieldValue = assetFlexContext[fieldName];
  721. const isArray = Array.isArray(fieldValue);
  722. const isKeyValueObject = typeof fieldValue === "object" &&
  723. fieldValue !== null &&
  724. !Array.isArray(fieldValue) && // Exclude arrays
  725. Object.keys(fieldValue).length > 0;
  726. if (isArray) {
  727. const sensorIndex = assetFlexContext[fieldName].indexOf(sensorId);
  728. assetFlexContext[fieldName].splice(sensorIndex, 1);
  729. } else if (isKeyValueObject) {
  730. assetFlexContext[fieldName] = "";
  731. }
  732. } else {
  733. assetFlexContext[fieldName] = null;
  734. }
  735. renderFlexContextForm();
  736. }
  737. async function renderFlexContextInputField(fieldName) {
  738. senSearchResEle.innerHTML = "";
  739. let fieldValue = assetFlexContext[fieldName]
  740. let newFieldValue;
  741. let sensorsContent = [];
  742. if (fieldValue === null) {
  743. newFieldValue = ""
  744. } else if (typeof fieldValue === "object") {
  745. if (fieldValue["sensor"]) {
  746. newFieldValue = `{&quot;sensor&quot;: ${fieldValue["sensor"]}}`
  747. isSensorValue = true;
  748. } else if (fieldValue.length > 0) {
  749. newFieldValue = fieldValue.join(", ")
  750. }
  751. } else if (typeof fieldValue == "string") {
  752. newFieldValue = fieldValue
  753. }
  754. const isString = typeof fieldValue === "string";
  755. const isArray = Array.isArray(fieldValue);
  756. const isKeyValueObject = typeof fieldValue === "object" &&
  757. fieldValue !== null &&
  758. !Array.isArray(fieldValue) && // Exclude arrays
  759. Object.keys(fieldValue).length > 0;
  760. const InputTitle = getFlexContextFieldTitle(fieldName);
  761. if (isKeyValueObject) {
  762. const sensors = [fieldValue["sensor"]]
  763. sensorsContent = await Promise.all(
  764. sensors.map(async (sensor, sensorIndex) => {
  765. const sensorData = await getSensor(fieldValue["sensor"]);
  766. const Asset = await getAsset(sensorData.generic_asset_id);
  767. const Account = await getAccount(Asset.account_id);
  768. return `
  769. <div class="p-1 mb-3">
  770. <div class="d-flex justify-content-between">
  771. <div>
  772. <b>Sensor:</b> <a href="${apiBasePath}/sensors/${sensorData.id}">${sensorData.id}</a>,
  773. <b>Unit:</b> ${sensorData.unit},
  774. <b>Name:</b> ${sensorData.name},
  775. <div style="padding-top: 1px;"></div>
  776. <b>Asset:</b> ${Asset.name},
  777. <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
  778. </div>
  779. </div>
  780. </div>`;
  781. }));
  782. } else if (isArray) {
  783. sensorsContent = await Promise.all(
  784. fieldValue.map(async (sensor, sensorIndex) => {
  785. const sensorData = await getSensor(sensor);
  786. const Asset = await getAsset(sensorData.generic_asset_id);
  787. const Account = await getAccount(Asset.account_id);
  788. return `
  789. <div class="p-1 mb-3">
  790. <div class="d-flex justify-content-between">
  791. <div>
  792. <b>Sensor:</b> <a href="${apiBasePath}/sensors/${sensorData.id}">${sensorData.id}</a>,
  793. <b>Unit:</b> ${sensorData.unit},
  794. <b>Name:</b> ${sensorData.name},
  795. <div style="padding-top: 1px;"></div>
  796. <b>Asset:</b> ${Asset.name},
  797. <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
  798. </div>
  799. <i class="fa fa-times"
  800. onclick="removeFlexContextSensorValue('${fieldName}', ${sensorData.id})"
  801. data-bs-toggle="tooltip"
  802. data-bs-placement="top"
  803. title="Remove Sensor"
  804. style="cursor: pointer;"
  805. ></i>
  806. </div>
  807. </div>`;
  808. }));
  809. }
  810. return `
  811. <div class="row g-2 my-1 card-highlight p-1 ${isString ? "bg-fixed-val" : "bg-dynamic-val"}" id="${fieldName}-control">
  812. <div class="mb-2 d-flex justify-content-between align-items-center pb-2">
  813. <label id="${fieldName}-label" for="${fieldName}-control-input" class="form-label mb-0 fs-5 fw-semi-bold">${InputTitle}</label>
  814. <i class="fa fa-times"
  815. onclick="removeFlexContextField('${fieldName}')"
  816. data-bs-toggle="tooltip"
  817. data-bs-placement="top"
  818. title="Unset this field"
  819. style="cursor: pointer;"
  820. ></i>
  821. </div>
  822. ${isKeyValueObject || isArray ? `
  823. <div>
  824. ${sensorsContent.length > 0 ? sensorsContent.join("") : `<div class="alert alert-warning" role="alert"> No sensors added to this field. Add sensors by selecting them from the right.</div>`}
  825. </div>
  826. ` : ""}
  827. ${isString ? `
  828. <div class="shadow-sm pb-2 px-2">
  829. <span class="fw-light fs-4">${newFieldValue ? newFieldValue : "<i>Not Set</i>"}</span>
  830. </div>
  831. ` : ""}
  832. </div>
  833. `;
  834. }
  835. async function renderFlexContextForm() {
  836. flexContextFormEle.innerHTML = "";
  837. for (const [key, value] of Object.entries(assetFlexContext)) {
  838. if (value !== null) {
  839. flexContextFormEle.innerHTML += await renderFlexContextInputField(key);
  840. }
  841. }
  842. renderFlexSelectOptions();
  843. selectFlexField();
  844. renderSelectInfoCards();
  845. }
  846. async function udpateFlexContextFieldValue(dataType, sensorId) {
  847. let highlightedCard = document.querySelector('.border-on-click');
  848. if (!highlightedCard) {
  849. showToast("Please select a field to update", "error");
  850. return;
  851. }
  852. // get card id and remove '-control' at the end
  853. const cardId = highlightedCard.id;
  854. const flexId = cardId.slice(0, -8);
  855. if (dataType === "fixedValue") {
  856. const powerCapacityInputValue = document.getElementById("fixed-value").value;
  857. // check if value is valid
  858. if(powerCapacityInputValue === "") {
  859. showToast("Please enter a value", "error");
  860. return;
  861. }
  862. assetFlexContext[flexId] = powerCapacityInputValue;
  863. } else if (dataType === "sensor" && sensorId) {
  864. if (flexId === "inflexible-device-sensors") {
  865. if (assetFlexContext[flexId]) {
  866. assetFlexContext[flexId].push(sensorId);
  867. } else {
  868. assetFlexContext[flexId] = [sensorId];
  869. }
  870. } else {
  871. assetFlexContext[flexId] = { sensor: sensorId };
  872. }
  873. }
  874. highlightedCard = await renderFlexContextInputField(flexId);
  875. flexContextFormEle.innerHTML = flexContextFormEle.innerHTML.replace(document.getElementById(`${flexId}-control`).outerHTML, highlightedCard);
  876. await selectFlexField(document.getElementById(`${flexId}-control`));
  877. // Update FlexContext
  878. await updateFlexContext();
  879. }
  880. async function addFlexContextField() {
  881. const fieldName = flexSelect.value;
  882. if (fieldName === "blank") {
  883. showToast("Please select a field to add", "error");
  884. return;
  885. }
  886. assetFlexContext[fieldName] = "";
  887. flexOptionsContainer.innerHTML = "";
  888. await renderFlexContextForm(); // im rendring here so te card with the 'border-on-click' class is updated/exists
  889. const card = document.getElementById(`${fieldName}-control`);
  890. selectFlexField(card);
  891. }
  892. function getFlexContextFieldTitle(fieldName){
  893. return fieldName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
  894. }
  895. function renderFlexSelectOptions() {
  896. let assetFlexContextTemplate = {
  897. "consumption-price": null,
  898. "production-price": null,
  899. "site-power-capacity": null,
  900. "site-production-capacity": null,
  901. "site-consumption-capacity": null,
  902. "site-consumption-breach-price": null,
  903. "site-production-breach-price": null,
  904. "site-peak-consumption": null,
  905. "site-peak-consumption-price": null,
  906. "site-peak-production": null,
  907. "site-peak-production-price": null,
  908. "inflexible-device-sensors": []
  909. }
  910. // comapare the assetFlexContext with the template and get the fields that are not set
  911. assetFlexContextTemplate = Object.assign(assetFlexContextTemplate, assetFlexContext);
  912. flexSelect.innerHTML = `<option value="blank">Select an option</option>`;
  913. for (const [key, value] of Object.entries(assetFlexContextTemplate)) {
  914. if (value === null || value.length === 0) {
  915. flexSelect.innerHTML += `<option value="${key}">${getFlexContextFieldTitle(key)}</option>`;
  916. }
  917. }
  918. }
  919. function renderFlexInputOptions(contextKey, sensorsOnly = false) {
  920. return `
  921. <div class="card-header">
  922. <ul class="nav nav-tabs card-header-tabs" id="pills-tab" role="tablist">
  923. ${sensorsOnly ? `
  924. <li class="nav-item" role="presentation">
  925. <button class="nav-link active bg-dynamic-val" id="pills-dynamic-tab" data-bs-toggle="pill" data-bs-target="#pills-dynamic" type="button" role="tab" aria-controls="pills-dynamic" aria-selected="false">
  926. Dynamic value (a sensor)
  927. </button>
  928. </li>
  929. ` : `
  930. <li class="nav-item" role="presentation">
  931. <button class="nav-link active bg-fixed-val" id="pills-homfixed" data-bs-toggle="pill" data-bs-target="#pills-fixed" type="button" role="tab" aria-controls="pills-fixed" aria-selected="true">
  932. Fixed value
  933. </button>
  934. </li>
  935. <li class="nav-item" role="presentation">
  936. <button class="nav-link bg-dynamic-val" id="pills-dynamic-tab" data-bs-toggle="pill" data-bs-target="#pills-dynamic" type="button" role="tab" aria-controls="pills-dynamic" aria-selected="false">
  937. Dynamic value (a sensor)
  938. </button>
  939. </li>
  940. `}
  941. </ul>
  942. <div class="tab-content" id="pills-tabContent">
  943. ${sensorsOnly ? `
  944. <div class="tab-pane fade show active bg-dynamic-val rounded-bottom" id="pills-dynamic" role="tabpanel" aria-labelledby="pills-dynamic-tab">
  945. <div id="select-sensor-input" class="flex-input-group mb-4 p-2">
  946. <label for="sensor" class="form-label">Look up Sensor for <b>${getFlexContextFieldTitle(contextKey)} </b>
  947. <i class="fa fa-info-circle ps-2"
  948. data-bs-toggle="tooltip"
  949. data-bs-placement="bottom"
  950. title="Search for sensor(s) to add here. Make sure that the sensor data will be available consistently, though, so scheduling will work."
  951. ></i>
  952. </label>
  953. <div class="form-check">
  954. <input class="form-check-input" type="checkbox" value="" id="flexCheckBox" onchange="searchSensorsFlexContextForm()"/>
  955. <label class="form-check-label" for="flexCheckBox">
  956. Include Public Assets
  957. </label>
  958. </div>
  959. <div class="row">
  960. <div class="col-8 pe-1">
  961. <input
  962. type="text"
  963. id="flexContextSensorSearch"
  964. class="form-control"
  965. placeholder="Search sensors..."
  966. oninput="searchSensorsFlexContextForm()"
  967. />
  968. </div>
  969. <div class="col-4 ps-0">
  970. <select id="flexUnitsSelect" class="form-select" onchange="searchSensorsFlexContextForm()">
  971. <option value="${null}" selected>Units</option>
  972. ${availableUnits.map( unit => '<option value="' + unit + '">' + unit + '</option>').join('')}
  973. </select>
  974. </div>
  975. </div>
  976. </div>
  977. </div>
  978. ` : `
  979. <div class="tab-pane fade show active bg-fixed-val rounded-bottom" id="pills-fixed" role="tabpanel" aria-labelledby="pills-fixed-tab">
  980. <div class="flex-input-group mb-4 p-2">
  981. <label for="fixed-value" class="form-label">Value for <b>${getFlexContextFieldTitle(contextKey)} </b>
  982. <i class="fa fa-info-circle ps-2"
  983. data-bs-toggle="tooltip"
  984. data-bs-placement="bottom"
  985. title="Enter a fixed value with a unit, e.g. '75kW' or '105 EUR'. It will be used in all schedules for this asset and his sensors."
  986. ></i>
  987. </label>
  988. <input type="text" id="fixed-value" class="form-control"
  989. value="${typeof(assetFlexContext[contextKey]) == 'string' ? assetFlexContext[contextKey] : ""}"
  990. placeholder="e.g. 15000 kW"
  991. onkeydown="if(event.key === 'Enter') { udpateFlexContextFieldValue('fixedValue', ${null}) }"
  992. >
  993. <button class="btn btn-secondary btn-sm me-2 mt-2"
  994. onclick="udpateFlexContextFieldValue('fixedValue', ${null})">Set Value</button>
  995. </div>
  996. </div>
  997. <div class="tab-pane fade bg-dynamic-val rounded-bottom" id="pills-dynamic" role="tabpanel" aria-labelledby="pills-dynamic-tab">
  998. <div id="select-sensor-input" class="flex-input-group mb-4 p-2">
  999. <label for="sensor" class="form-label">Look up Sensor for <b>${getFlexContextFieldTitle(contextKey)} </b>
  1000. <i class="fa fa-info-circle ps-2"
  1001. data-bs-toggle="tooltip"
  1002. data-bs-placement="bottom"
  1003. title="You can add a sensor which provides the values in a dynamic fashion. In the search form below, you can choose the right one. Make sure that the sensor data will be available consistently, though, so scheduling will work."
  1004. ></i>
  1005. </label>
  1006. <div class="form-check">
  1007. <input class="form-check-input" type="checkbox" value="" id="flexCheckBox"/>
  1008. <label class="form-check-label" for="flexCheckBox">
  1009. Include Public Assets
  1010. </label>
  1011. </div>
  1012. <div class="row">
  1013. <div class="col-8 pe-1">
  1014. <input
  1015. type="text"
  1016. id="flexContextSensorSearch"
  1017. class="form-control"
  1018. placeholder="Search sensors..."
  1019. oninput="searchSensorsFlexContextForm()"
  1020. />
  1021. </div>
  1022. <div class="col-4 ps-0">
  1023. <select id="flexUnitsSelect" class="form-select" onchange="searchSensorsFlexContextForm()">
  1024. <option value="${null}" selected>Units</option>
  1025. ${availableUnits.map( unit => '<option value="' + unit + '">' + unit + '</option>').join('')}
  1026. </select>
  1027. </div>
  1028. </div>
  1029. </div>
  1030. </div>
  1031. `}
  1032. </div>
  1033. </div>
  1034. `;
  1035. }
  1036. function selectFlexField(highlightedCard) {
  1037. if (highlightedCard) {
  1038. if (highlightedCard.classList.contains("border-on-click")) {
  1039. // Pass
  1040. } else {
  1041. document.querySelectorAll(".card-highlight").forEach(el => el.classList.remove("border-on-click"));
  1042. flexOptionsContainer.innerHTML = "";
  1043. highlightedCard.classList.add("border-on-click");
  1044. const cardLabel = highlightedCard.querySelector("label");
  1045. if (cardLabel) {
  1046. // remove "-label" at the end leaing the flexcontext key
  1047. const flexKey = cardLabel.id.slice(0, -6);
  1048. handleFlexSelectChange(flexKey);
  1049. if (flexKey === "inflexible-device-sensors" || flexKey === "consumption-price" || flexKey === "production-price") {
  1050. flexOptionsContainer.innerHTML = renderFlexInputOptions(flexKey, true);
  1051. } else {
  1052. flexOptionsContainer.innerHTML = renderFlexInputOptions(flexKey);
  1053. }
  1054. }
  1055. }
  1056. } else {
  1057. document.querySelectorAll(".card-highlight").forEach(el => el.classList.remove("border-on-click"));
  1058. flexOptionsContainer.innerHTML = "";
  1059. }
  1060. }
  1061. // ============== Flex Context Management END ============== //
  1062. // ============== Page Events ============== //
  1063. flexContexFormModal.addEventListener('shown.bs.modal', function () {
  1064. // Initial renders
  1065. renderFlexContextForm(); // Initial render of flex context form
  1066. });
  1067. flexContexFormModal.addEventListener('hidden.bs.modal', function () {
  1068. updateFlexContext();
  1069. });
  1070. document.addEventListener("click", function (event) {
  1071. /**
  1072. The logic in this block is majorly to remove the border on click of the card and add it to the selected card
  1073. but as this event is added to the document, it will be triggered on any click event on the page
  1074. so the if statements are used to check if the click event is on the card or not
  1075. */
  1076. const card = event.target.closest(".card-highlight");
  1077. const sensorCard = event.target.closest(".sensor-card");
  1078. const searchInput = event.target.id === "searchInput";
  1079. const cardBody = event.target.closest(".card-body");
  1080. const editTitleBtn = event.target.id === "editTitleBtn";
  1081. const saveTitleBtn = event.target.id === "saveTitleBtn";
  1082. const unSetBtn = event.target.id === "unSetFlexField";
  1083. if (card) {
  1084. if (card.classList.contains("border-on-click")) {
  1085. if (editTitleBtn || saveTitleBtn) {
  1086. renderGraphCards();
  1087. }
  1088. // Pass
  1089. } else {
  1090. selectFlexField(card)
  1091. renderGraphCards();
  1092. }
  1093. } else if (
  1094. cardBody !== null && cardBody !== undefined ||
  1095. sensorCard !== null && sensorCard !== undefined ||
  1096. searchInput !== null && searchInput !== undefined
  1097. ) {
  1098. // Pass
  1099. } else {
  1100. document.querySelectorAll(".card-highlight").forEach(el => el.classList.remove("border-on-click"));
  1101. renderApiSensors([], undefined);
  1102. }
  1103. });
  1104. function renderSelectInfoCards(contextKey, description, allowedUnits, isArray = false) {
  1105. if (contextKey) {
  1106. return `
  1107. <div>
  1108. <div class="alert alert-info" role="alert">
  1109. <b> About ${getFlexContextFieldTitle(contextKey)}
  1110. <div class="pt-2 fw-normal">
  1111. ${description}
  1112. </div>
  1113. <div class="pt-3 fw-bold">
  1114. Example Units: <span class="fw-normal"> ${allowedUnits.join(", ")} </span>
  1115. </div>
  1116. </div>
  1117. </div>
  1118. `;
  1119. } else {
  1120. flexInfoContainer.innerHTML = "";
  1121. }
  1122. }
  1123. function handleFlexSelectChange(selectedOption) {
  1124. flexInfoContainer.innerHTML = "";
  1125. const alertStyle = "alert alert-light";
  1126. if (selectedOption === "consumption-price") {
  1127. const description = "Set the sensor that represents the consumption price of the site. This value will be used in the optimization";
  1128. const allowedUnits = ["EUR/MWh", "JPY/kWh", "USD/MWh, and other currencies."];
  1129. flexInfoContainer.innerHTML = renderSelectInfoCards("consumption-price", description, allowedUnits);
  1130. } else if (selectedOption === "production-price") {
  1131. const description = "Set the sensor that represents the production price of the site. This value will be used in the optimization";
  1132. const allowedUnits = ["EUR/MWh", "JPY/kWh", "USD/MWh, and other currencies."];
  1133. flexInfoContainer.innerHTML = renderSelectInfoCards("production-price", description, allowedUnits);
  1134. } else if (selectedOption == "site-power-capacity") {
  1135. const description = "This value represents the maximum power that the site can consume or produce. This value will be used in the optimization";
  1136. const allowedUnits = ["kW", "kVA", "MVA"];
  1137. flexInfoContainer.innerHTML = renderSelectInfoCards("site-power-capacity", description, allowedUnits);
  1138. } else if (selectedOption == "site-production-capacity") {
  1139. const description = "This value represents the maximum power that the site can produce. This value will be used in the optimization";
  1140. const allowedUnits = ["kW"];
  1141. flexInfoContainer.innerHTML = renderSelectInfoCards("site-production-capacity", description, allowedUnits);
  1142. } else if (selectedOption == "site-consumption-capacity") {
  1143. const description = "This value represents the maximum power that the site can consume. This value will be used in the optimization";
  1144. const allowedUnits = ["kW"];
  1145. flexInfoContainer.innerHTML = renderSelectInfoCards("site-consumption-capacity", description, allowedUnits);
  1146. } else if (selectedOption == "site-consumption-breach-price") {
  1147. const description = "This value represents the price that will be paid if the site consumes more power than the site consumption capacity. This value will be used in the optimization";
  1148. const allowedUnits = ["EUR/MW", "JPY/kW", "USD/MW, and other currencies."];
  1149. flexInfoContainer.innerHTML = renderSelectInfoCards("site-consumption-breach-price", description, allowedUnits);
  1150. } else if (selectedOption == "site-production-breach-price") {
  1151. const description = "This value represents the price that will be paid if the site produces more power than the site production capacity. This value will be used in the optimization";
  1152. const allowedUnits = ["EUR/MW", "JPY/kW", "USD/MW, and other currencies."];
  1153. flexInfoContainer.innerHTML = renderSelectInfoCards("site-production-breach-price", description, allowedUnits);
  1154. } else if (selectedOption == "site-peak-consumption") {
  1155. const description = "This value represents the peak consumption of the site. This value will be used in the optimization";
  1156. const allowedUnits = ["kW"];
  1157. flexInfoContainer.innerHTML = renderSelectInfoCards("site-peak-consumption", description, allowedUnits);
  1158. } else if (selectedOption == "site-peak-production") {
  1159. const description = "This value represents the peak production of the site. This value will be used in the optimization";
  1160. const allowedUnits = ["kW"];
  1161. flexInfoContainer.innerHTML = renderSelectInfoCards("site-peak-production", description, allowedUnits);
  1162. } else if (selectedOption == "site-peak-consumption-price") {
  1163. const description = "This value represents the price that will be paid if the site consumes more power than the site peak consumption. This value will be used in the optimization";
  1164. const allowedUnits = ["EUR/MW", "JPY/kW", "USD/MW, and other currencies."];
  1165. flexInfoContainer.innerHTML = renderSelectInfoCards("site-peak-consumption-price", description, allowedUnits);
  1166. } else if (selectedOption == "site-peak-production-price") {
  1167. const description = "This value represents the price that will be paid if the site produces more power than the site peak production. This value will be used in the optimization";
  1168. const allowedUnits = ["EUR/MW", "JPY/kW", "USD/MW, and other currencies."];
  1169. flexInfoContainer.innerHTML = renderSelectInfoCards("site-peak-production-price", description, allowedUnits);
  1170. } else if (selectedOption == "inflexible-device-sensors") {
  1171. const description = "This value represents the sensors that are inflexible and cannot be controlled. These sensors will be used in the optimization";
  1172. const allowedUnits = ["kW"];
  1173. flexInfoContainer.innerHTML = renderSelectInfoCards("inflexible-device-sensors", description, allowedUnits);
  1174. }
  1175. }
  1176. flexSelect.addEventListener('change', function () {
  1177. handleFlexSelectChange(flexSelect.value);
  1178. });
  1179. // ============== Page Events ============== //
  1180. </script>
  1181. {% endblock %}