12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361 |
- {% extends "base.html" %}
- {% set active_page = "assets" %}
- {% block title %} {{ asset.name }} - Scenario {% endblock %}
- {% block divs %}
- {% block breadcrumbs %} {{ super() }} {% endblock %}
- <div class="container-fluid">
- <div class="row">
- <div class="col-sm-2"></div>
- <div class="col-sm-8 card">
- <div class="row">
- <div class="col-sm-9">
- <div class="d-flex justify-content-between align-items-center" style="margin-bottom: 10px;">
- <h1>
- Asset: {{ asset.name }}
- </h1>
- <div class="d-flex">
- <button class="btn btn-sm btn-primary me-2" onclick='openSensorsModal()'>
- Show sensors
- </button>
- <button class="btn btn-sm btn-primary me-2" data-bs-toggle="modal"
- data-bs-target="#flexContextModal">
- Edit flex-context
- </button>
- </div>
- </div>
- <p>Type: {{ asset.generic_asset_type.name.split('.')[-1] | title }}</p>
- </div>
- <div class="col-sm-3">
- {% if can_delete %}
- <button type="button" class="btn delete-button" onClick="delete_asset(event, {{ asset.id }}, '{{ asset.name }}')">Delete Scenario</button>
- {% endif %}
- <!-- Tabs for toggling content -->
- <div class="tabs-container">
- <ul class="nav nav-tabs">
- <!-- Reorder the tabs based on the length of the assets -->
- {% if assets|length > 2 %}
- <!-- Structure tab will be first if assets length > 2 -->
- <li class="nav-item">
- <a class="nav-link active" id="structure-tab" data-bs-toggle="tab" href="#structure">Structure</a>
- </li>
- <li class="nav-item">
- <a class="nav-link" id="location-tab" data-bs-toggle="tab" href="#location">Location</a>
- </li>
- {% else %}
- <!-- Location tab will be first if assets length <= 2 -->
- <li class="nav-item">
- <a class="nav-link active" id="location-tab" data-bs-toggle="tab" href="#location">Location</a>
- </li>
- <li class="nav-item">
- <a class="nav-link" id="structure-tab" data-bs-toggle="tab" href="#structure">Structure</a>
- </li>
- {% endif %}
- </ul>
- </div>
- </div>
- </div>
-
- <!-- Tab Content (outside col-sm) -->
- <div class="tab-content">
- <!-- Structure Content -->
- <div class="tab-pane fade {% if assets|length > 2 %}show active{% endif %}" id="structure">
- <div class="col-sm-12">
- {% from "_macros.html" import show_tree %}
- {{ show_tree(assets, asset.name) }}
- </div>
- </div>
-
- <!-- Location Content -->
- <div class="tab-pane fade {% if assets|length <= 2 %}show active{% endif %}" id="location">
- <!-- Location-related content here -->
- <div id="mapid" style="height: 400px;"></div>
- </div>
- </div>
- </div>
- <div class="col-sm-2"></div>
-
- <!-- Bootstrap Modal for Sensors -->
- <div class="modal fade" id="sensorsModal" tabindex="-1" aria-labelledby="modalTitle" aria-hidden="true">
- <div class="modal-dialog modal-lg">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title" id="modalTitle">Node Details</h5>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <table id="modalTable" class="table table-bordered">
- <thead id="modalTableHead"></thead>
- <tbody id="modalTableBody"></tbody>
- </table>
- </div>
- <div class="modal-footer">
- <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- Flex Context Modal -->
- <div class="modal fade modal-xl" id="flexContextModal" tabindex="-1" aria-labelledby="flexContextModalLabel"
- aria-hidden="true">
- <div class="modal-dialog">
- <div class="modal-content">
- <div class="modal-header">
- <h5 class="modal-title pe-2">Edit Asset's FlexContext</h5>
- <div class="dropdown" data-bs-auto-close="outside">
- <span class="fa fa-info dropdown-toggle" role="button" data-bs-toggle="dropdown"
- aria-expanded="false" rel="tooltip" aria-hidden="true" tabindex="0"
- data-bs-placement="right" data-bs-toggle="tooltip"></span>
- <div class="dropdown-menu p-3" style="width: 400px;">
- <div>
- <strong>Help</strong>
- <p class="mb-2 pt-2">
- Here, you can edit the flexibility context for this asset. This describes information about the managed system as a
- 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>.
- <br/>
- <br/>
- These fields can also be sent via the API to FlexMeasures, but setting them here permanently might be more convenient.
- 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).
- <br/>
- <br/>
- To add a field, choose it from the dropdown menu in the top right corner, then click the "Add Field" button'.
- Set a value in the right panel, using the provided form to set the field's value.
- </p>
- </div>
- </div>
- </div>
- <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
- </div>
- <div class="modal-body">
- <div class="row">
- <!-- Left Column -->
- <div class="col-5">
- <form id="flexContextForm">
- </form>
- </div>
- <!-- Right Column -->
- <div class="col-7">
- <div>
- <!-- Loading Spinner -->
- <div id="spinnerElement" class="d-flex justify-content-center align-items-center mb-2"
- style="height: 50vh; display: none !important;">
- <div class="spinner-border text-primary" role="status"
- style="width: 4rem; height: 4rem;">
- <span class="visually-hidden">Loading...</span>
- </div>
- </div>
- <!-- Settings -->
- <div>
- <div class="row">
- <div class="col-10">
- <select class="form-select" id="flexSelect">
- <option value="blank">Select flex-context field to add</option>
- </select>
- </div>
- <div class="col-2 p-0">
- <button class="btn btn-secondary btn-md"
- onclick="addFlexContextField()">Add Field</button>
- </div>
- </div>
- <div id="flexInfoContainer" class="mt-3"></div>
- </div>
- <hr class="border border-dark" />
- <div id="flexOptionsContainer" class="mt-3"></div>
- <div id="sensorsSearchResults" class="row mt-3" style="display: none;"></div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <script>
- function openSensorsModal() {
- const sensors = {{ current_asset_sensors | safe }};
- document.getElementById("modalTitle").innerText = "Sensors for " + '{{ asset.name }}';
- let tableHead = document.getElementById("modalTableHead");
- let tableBody = document.getElementById("modalTableBody");
- // Clear previous content
- tableHead.innerHTML = "";
- tableBody.innerHTML = "";
- if (sensors && sensors.length > 0) {
- let keys = Object.keys(sensors[0]); // e.g., ['name', 'unit']
- keys.pop("link"); // Remove the 'link' from the table
-
- // Create table header row
- let headerRow = document.createElement("tr");
- keys.forEach(key => {
- let headerText = key.charAt(0).toUpperCase() + key.slice(1);
- let th = document.createElement("th"); // Use <th> for header cells
- th.innerText = headerText;
- th.setAttribute("aria-label", headerText); // set aria-label so floatThead can pick it up
- headerRow.appendChild(th);
- });
- tableHead.appendChild(headerRow);
- // Force destroy the floatThead instance
- setTimeout(function(){
- if ($.fn.floatThead) {
- $('#modalTable').floatThead('destroy');
- }
- }, 200);
- // Populate table with sensor data
- sensors.forEach(sensor => {
- let row = document.createElement("tr");
- keys.forEach(key => {
- let td = document.createElement("td");
- if (key === "name") {
- // Create an anchor element
- let a = document.createElement("a");
- a.innerText = sensor[key] !== undefined ? sensor[key] : "N/A";
- // Set the href; you can modify this as needed, for example using sensor.link if available
- a.href = sensor.link;
- td.appendChild(a);
- } else {
- td.innerText = sensor[key] !== undefined ? sensor[key] : "N/A";
- }
- row.appendChild(td);
- });
- tableBody.appendChild(row);
- });
- } else {
- tableBody.innerHTML = "<tr><td colspan='100%'>No sensor data available</td></tr>";
- }
- // Show Bootstrap modal
- let modal = new bootstrap.Modal(document.getElementById('sensorsModal'));
- modal.show();
- }
- </script>
- <style>
- details {
- display: none;
- }
- </style>
- <!-- Initialise the map -->
- <script src="https://cdn.jsdelivr.net/npm/leaflet@1.7.1/dist/leaflet-src.min.js"></script>
- <script src="{{ url_for('flexmeasures_ui.static', filename='js/map-init.js') }}"></script>
- <script type="text/javascript">
- // create map
- var assetMap = L
- .map('mapid', { center: [{{ asset.latitude | replace("None", 10) }}, {{ asset.longitude | replace("None", 10) }}], zoom: 10})
- .on('popupopen', function () {
- $(function () {
- $('[data-toggle="tooltip"]').tooltip();
- });
- });
- addTileLayer(assetMap, '{{ mapboxAccessToken }}');
- // create marker
- var asset_icon = new L.DivIcon({
- className: 'map-icon',
- 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>',
- iconSize: [100, 100], // size of the icon
- iconAnchor: [50, 50], // point of the icon which will correspond to marker's location
- popupAnchor: [0, -50] // point from which the popup should open relative to the iconAnchor
- });
- var marker = L
- .marker(
- [{{ asset.latitude | replace("None", 10) }}, {{ asset.longitude | replace("None", 10) }}],
- { icon: asset_icon }
- ).addTo(assetMap);
- </script>
- <script>
- const senSearchResEle = document.getElementById("sensorsSearchResults")
- const flexContextFormEle = document.getElementById("flexContextForm")
- const flexContexFormModal = document.getElementById('flexContextModal');
- const flexSelect = document.getElementById('flexSelect');
- const flexInfoContainer = document.getElementById('flexInfoContainer');
- const flexOptionsContainer = document.getElementById('flexOptionsContainer');
- let availableUnitsRawJSON = "{{ available_units | safe }}";
- availableUnitsRawJSON = availableUnitsRawJSON.replace(/'/g, '"');
- const availableUnits = JSON.parse(availableUnitsRawJSON);
- let assetFlexContext = {
- "consumption-price": null,
- "production-price": null,
- "site-power-capacity": null,
- "site-production-capacity": null,
- "site-consumption-capacity": null,
- "site-consumption-breach-price": null,
- "site-production-breach-price": null,
- "site-peak-consumption": null,
- "site-peak-consumption-price": null,
- "site-peak-production": null,
- "site-peak-production-price": null,
- "inflexible-device-sensors": []
- }
- let assetFlexContextRawJSON = "{{ asset.flex_context | safe }}";
- assetFlexContextRawJSON = processAssetRawFlexContextJSON(assetFlexContextRawJSON);
-
- const apiSensorsListElement = document.getElementById("apiSensorsList");
- const spinnerElement = document.getElementById('spinnerElement');
- let sensorsToShowRawJSON = "{{ asset.sensors_to_show | safe }}";
- sensorsToShowRawJSON = sensorsToShowRawJSON.replace(/'/g, '"');
- let sensorsToShow = JSON.parse(sensorsToShowRawJSON);
- let cachedFilteredSensors = []; // keeps track of the filtered sensors
- let editingIndex; // keeps track of which graph we are currently editing
- let savedGraphIndex; // keeps track of the graph that is currently selected
- let selectedGraphTitle; // keeps track of the graph title that is currently selected
- const apiBasePath = window.location.origin;
- const sensorsApiUrl = `${apiBasePath}/api/v3_0/sensors?page=1&per_page=100&asset_id={{ asset.id }}`;
- function processAssetRawFlexContextJSON(rawJSON){
- let processedJSON = rawJSON.replace(/'/g, '"');
- // change None to null
- processedJSON = processedJSON.replace(/None/g, 'null');
- // update the assetFlexContext fields
- processedJSON = JSON.parse(processedJSON);
- for (const [key, value] of Object.entries(processedJSON)) {
- if (key in assetFlexContext) {
- assetFlexContext[key] = processedJSON[key];
- } else {
- assetFlexContext[key] = null;
- }
- }
- return processedJSON;
- }
- // Fetch Account Details
- async function getAccount(accountId) {
- const cacheKey = `account_${accountId}`;
- const cachedData = localStorage.getItem(cacheKey);
- if (cachedData) {
- return JSON.parse(cachedData);
- }
- const apiUrl = apiBasePath + "/api/v3_0/accounts/" + accountId;
- const response = await fetch(apiUrl);
- const account = await response.json();
- localStorage.setItem(cacheKey, JSON.stringify(account));
- return account;
- }
- // Fetch Asset Details
- async function getAsset(assetId, useCache = true) {
- const cacheKey = `asset_${assetId}`;
- const cachedData = localStorage.getItem(cacheKey);
- if (cachedData && useCache) {
- return JSON.parse(cachedData);
- }
- const apiUrl = apiBasePath + "/api/v3_0/assets/" + assetId;
- const response = await fetch(apiUrl);
- const asset = await response.json();
- localStorage.setItem(cacheKey, JSON.stringify(asset));
- return asset;
- }
- // Fetch Sensor Details
- async function getSensor(id) {
- const cacheKey = `sensor_${id}`;
- const cachedData = localStorage.getItem(cacheKey);
- if (cachedData) {
- return JSON.parse(cachedData);
- }
- const apiUrl = apiBasePath + "/api/v3_0/sensors/" + id;
- const response = await fetch(apiUrl);
- const sensor = await response.json();
- localStorage.setItem(cacheKey, JSON.stringify(sensor));
- return sensor;
- }
- // highlight selected graph
- async function selectGraph(graphIndex) {
- if (graphIndex !== undefined) {
- savedGraphIndex = graphIndex;
- selectedGraphTitle = sensorsToShow[graphIndex]?.title;
- // check if graphIndex still exists(This is because this function is called when the removed button is clicked as well)
- if (sensorsToShow[graphIndex]) {
- renderApiSensors(cachedFilteredSensors, graphIndex);
- } else {
- renderApiSensors(cachedFilteredSensors);
- }
- } else {
- savedGraphIndex = undefined;
- selectedGraphTitle = undefined;
- renderGraphCards();
- }
- }
- // Function to render the graph cards
- async function renderGraphCards() {
- const graphList = document.getElementById("graphList");
- graphList.innerHTML = "";
- const newSensorsToShow = [];
- if (sensorsToShow.length === 0) {
- return;
- }
- for (const [index, item] of sensorsToShow.entries()) {
- const col = document.createElement("div");
- col.classList.add("col-12", "mb-1");
- const sensorsUnits = [];
- // the initializing of the newItem is to handle the case where the item is an array of ID's instead of an object
- const newItem = {
- title: item.title ?? "No Title",
- sensors: item.sensors ?? (Array.isArray(item) ? item : [item]),
- }
- newSensorsToShow.push(newItem);
- const sensorsContent = await Promise.all(
- newItem.sensors.map(async (sensor, sensorIndex) => {
- const sensorData = await getSensor(sensor);
- const Asset = await getAsset(sensorData.generic_asset_id);
- const Account = await getAccount(Asset.account_id);
- sensorsUnits.push(sensorData.unit);
- return `
- <div class="p-1 mb-3 border-bottom border-secondary">
- <div class="d-flex justify-content-between">
- <div>
- <b>ID:</b> <a href="${apiBasePath}/sensors/${sensorData.id}">${sensorData.id}</a>,
- <b>Unit:</b> ${sensorData.unit},
- <b>Name:</b> ${sensorData.name},
- <div style="padding-top: 1px;"></div>
- <b>Asset:</b> ${Asset.name},
- <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
- </div>
- <i class="fa fa-times"
- onclick="removeSensorFromGraph(${index}, ${sensorIndex})"
- data-bs-toggle="tooltip"
- data-bs-placement="top"
- title="Remove Sensor"
- style="cursor: pointer;"
- ></i>
- </div>
- </div>`;
- }));
- const uniqueUnits = [...new Set(sensorsUnits)];
- col.innerHTML = `
- <div class="card m-0 p-1">
- <div class="card-body card-highlight ${newItem.title === selectedGraphTitle ? " border-on-click" : ""}" id="graph_${index}" onclick="selectGraph(${index})">
- <div class="d-flex align-items-center">
- <div>
- ${editingIndex === index
- ? `<input type="text" class="form-control mb-2 me-2" id="editTitle_${index}" value="${newItem.title}" onkeydown="handleEnterKeyEventForTitleEditing(event, ${index})" />`
- : `<h5 class="card-title me-2">${newItem.title}</h5>`
- }
- </div>
- <div>
- ${editingIndex === index
- ? `<button class="btn btn-success btn-sm ms-2" id="saveTitleBtn" onclick="saveGraphTitle(${index})">Save</button>`
- : `<button class="btn btn-warning btn-sm ms-2" id="editTitleBtn" onclick="editGraphTitle(${index})">Edit</button>`
- }
- </div>
- </div>
- <h5 class="card-title pt-2"><b> Sensors: </b></h5>
- <div>
- ${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>`}
- ${uniqueUnits.length > 1 ? `<div class="alert alert-warning" role="alert">Note that you are showing sensors with different units</div>` : ""}
- </div>
- <button class="btn btn-danger btn-sm me-2" onclick="(function(e) { e.stopPropagation(); removeGraph(${index}); })(event)">Remove</button>
- <button class="btn btn-secondary btn-sm me-2" onclick="(function(e) { e.stopPropagation(); moveGraphUp(${index}); })(event)" ${index === 0 ? "disabled" : ""}>Move Up</button>
- <button class="btn btn-secondary btn-sm" onclick="(function(e) { e.stopPropagation(); moveGraphDown(${index}); })(event)" ${index === sensorsToShow.length - 1 ? "disabled" : ""}>Move Down</button>
- </div>
- </div>`;
- graphList.appendChild(col);
- }
- sensorsToShow = newSensorsToShow;
- }
- /* This search function is tied to the sensorsToShow Form */
- async function searchSensorsGraphForm() {
- const searchValue = document.getElementById("searchInput").value.toLowerCase();
- const filterValue = document.getElementById("unitsSelect").value;
- const highlightedCard = document.querySelector('.border-on-click');
- spinnerElement.style.display = 'flex';
- apiSensorsListElement.style.display = 'none';
- // Due to the nature of async functions, the highlightedCard might not be available
- // when the filterSensors function is called. So, we need to check if it exists
- if (highlightedCard) {
- const cardId = highlightedCard.id;
- const index = cardId.split("_")[1];
- savedGraphIndex = index;
- selectedGraphTitle = sensorsToShow[index].title;
- } else {
- savedGraphIndex = undefined;
- selectedGraphTitle = undefined;
- }
- // check if apiSensorsList has been rendered
- if (apiSensorsListElement.innerHTML === "") {
- document.getElementById('apiSensorsList').style.display = 'block';
- }
- const params = new URLSearchParams();
- if (searchValue) {
- params.append('filter', searchValue);
- }
- if (filterValue !== "null") {
- params.append('unit', filterValue);
- }
- const apiUrl = `${sensorsApiUrl}&${params.toString()} `;
- try {
- const response = await fetch(apiUrl);
- if (!response.ok) {
- throw new Error('Failed to fetch sensors');
- }
- const responseData = await response.json();
- const filteredSensors = responseData.data;
- // Render the fetched sensors
- if (savedGraphIndex !== null && savedGraphIndex !== undefined) {
- renderApiSensors(filteredSensors, savedGraphIndex);
- } else {
- renderApiSensors(filteredSensors);
- }
- cachedFilteredSensors = filteredSensors;
- spinnerElement.classList.add('hidden-important');
- apiSensorsListElement.style.display = 'block';
- } catch (error) {
- const errorMessage = error?.message || error?.error || response.statusText;
- showToast(`Failed to search sensors: ${errorMessage}`, "error");
- }
- }
- /* This search function is tied to the FlexContext Form */
- async function searchSensorsFlexContextForm() {
- const searchValue = document.getElementById("flexContextSensorSearch").value.toLowerCase();
- spinnerElement.style.display = 'flex';
- senSearchResEle.style.display = 'none';
- const flexUnitsSelectValue = document.getElementById("flexUnitsSelect").value;
- const flexCheckBox = document.getElementById('flexCheckBox');
- const params = new URLSearchParams();
- if (searchValue) {
- params.append('filter', searchValue);
- }
- if (flexCheckBox.checked) {
- params.append('include_public_assets', true);
- }
- if(flexUnitsSelectValue != "null"){
- params.append('unit', flexUnitsSelectValue);
- }
- const apiUrl = `${sensorsApiUrl}&${params.toString()}`;
- try {
- const response = await fetch(apiUrl);
- if (!response.ok) {
- throw new Error('Failed to fetch sensors');
- }
- const responseData = await response.json();
- const filteredSensors = responseData.data;
- // Render the fetched sensors
- renderSensorSearchResults(filteredSensors);
- spinnerElement.classList.add('hidden-important');
- senSearchResEle.style.display = 'block';
- } catch (error) {
- const errorMessage = error?.message || error?.error || response.statusText;
- showToast(`Failed to search sensors: ${errorMessage}`, "error");
- }
- }
- // ============== Graph Cards Management ============== //
- async function removeGraph(index) {
- sensorsToShow.splice(index, 1);
- savedGraphIndex = undefined;
- selectedGraphTitle = undefined;
- editingIndex = undefined;
- renderGraphCards();
- renderApiSensors(cachedFilteredSensors);
- }
- async function swapItems(index1, index2) {
- if (index1 >= 0 && index2 >= 0 && index1 < sensorsToShow.length && index2 < sensorsToShow.length) {
- [sensorsToShow[index1], sensorsToShow[index2]] = [sensorsToShow[index2], sensorsToShow[index1]];
- renderGraphCards()
- renderApiSensors(cachedFilteredSensors, index2);
- }
- }
- async function moveGraphUp(index) {
- if (index > 0) {
- await swapItems(index, index - 1);
- }
- }
- async function moveGraphDown(index) {
- if (index < sensorsToShow.length - 1) {
- await swapItems(index, index + 1);
- }
- }
- function editGraphTitle(index) {
- editingIndex = index;
- }
- async function saveGraphTitle(index) {
- const newTitle = document.getElementById(`editTitle_${index}`).value;
- sensorsToShow[index].title = newTitle;
- editingIndex = null;
- selectedGraphTitle = newTitle;
- renderGraphCards();
- }
- // ============== Graph Cards Management ============== //
- // Render the available API sensors
- function renderApiSensors(sensors, graphIndex) {
- // graphIndex is undefined when the sensors are being added to the graph
- // graphIndex is defined when the sensors are being added to the graph cards. In other words
- // when more sensors are being added to single graph
- apiSensorsList.innerHTML = ""; // Clear the previous sensors
- if (sensors.length === 0) {
- apiSensorsList.innerHTML = "<h3>No sensors found</h3>";
- return;
- }
- sensors.forEach(async (sensor) => {
- const Asset = await getAsset(sensor.generic_asset_id);
- const Account = await getAccount(Asset.account_id);
- const col = document.createElement("div");
- col.classList.add("col-12", "mb-1");
- col.innerHTML = `
- <div class="card m-0">
- <div class="card-body p-0 sensor-card">
- <h5 class="card-title">${sensor.name}</h5>
- <p class="card-text">
- <b>ID:</b> <a href="${apiBasePath}/sensors/${sensor.id}">${sensor.id}</a>,
- <b>Unit:</b> ${sensor.unit},
- <b>Asset:</b> ${Asset.name},
- <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
- </p>
- ${graphIndex !== undefined && savedGraphIndex !== undefined && selectedGraphTitle !== undefined
- ? `<button class="btn btn-primary btn-sm" onclick="addSensorToExistingGraph(${graphIndex}, ${sensor.id})">Add to '${selectedGraphTitle}' Graph</button>`
- : `<button class="btn btn-primary btn-sm" onclick="addSensorAsGraph(${sensor.id})">Add new Graph</button>`
- }
- </div>
- </div>
- `;
- apiSensorsList.appendChild(col);
- });
- }
- function renderSensorSearchResults(sensors) {
- senSearchResEle.innerHTML = "";
- if (sensors.length === 0) {
- senSearchResEle.innerHTML = "<h4>No sensors found</h4>";
- return;
- }
- sensors.forEach(async (sensor) => {
- const Asset = await getAsset(sensor.generic_asset_id);
- const Account = await getAccount(Asset.account_id);
- const col = document.createElement("div");
- col.classList.add("col-12", "mb-1");
- col.innerHTML = `
- <div class="card m-0">
- <div class="card-body p-0 sensor-card">
- <h5 class="card-title">${sensor.name}</h5>
- <p class="card-text">
- <b>ID:</b> <a href="${apiBasePath}/sensors/${sensor.id}">${sensor.id}</a>,
- <b>Unit:</b> ${sensor.unit},
- <b>Asset:</b> ${Asset.name},
- <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
- </p>
- <button class="btn btn-primary btn-sm" onclick="udpateFlexContextFieldValue('sensor', ${sensor.id})">Add Sensor</button>
- </div>
- </div>
- `;
- senSearchResEle.appendChild(col);
- });
- }
- async function updateSensorToShow() {
- const apiURL = apiBasePath + "/api/v3_0/assets/{{ asset.id }}";
- const requestBody = JSON.stringify({ sensors_to_show: JSON.stringify(sensorsToShow) });
- // Show spinner
- document.getElementById('spinner').style.display = 'block';
- const response = await fetch(apiURL, {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- },
- body: requestBody,
- });
- if (!response.ok) {
- const errorData = await response.json();
- const errorMessage = errorData?.message || errorData?.error || response.statusText;
- showToast(`Failed to update sensors to show: ${errorMessage}`, "error");
- } else {
- showToast("Sensor graphs updated successfully", "success");
- }
- }
- async function updateFlexContext() {
- const apiURL = apiBasePath + "/api/v3_0/assets/{{ asset.id }}";
- // Clean null values
- for (const [key, value] of Object.entries(assetFlexContext)) {
- if (value === null) {
- delete assetFlexContext[key];
- }
- }
- const requestBody = JSON.stringify({ flex_context: JSON.stringify(assetFlexContext) });
- const response = await fetch(apiURL, {
- method: "PATCH",
- headers: {
- "Content-Type": "application/json",
- },
- body: requestBody,
- });
- if (!response.ok) {
- const errorData = await response.json();
- const errorMessage = errorData?.message || errorData?.error || response.statusText;
- showToast(`Failed to update the flex context: ${errorMessage}`, "error");
- // Revert to previous state
- const asset = await getAsset("{{ asset.id }}", false);
- const previousFlexContext = await processAssetRawFlexContextJSON(asset.flex_context);
- assetFlexContext = previousFlexContext;
- renderFlexContextForm();
- } else {
- showToast("Flex context updated successfully", "success");
- }
- }
- // Add a sensor as a new graph card
- function addSensorAsGraph(id) {
- const newAsset = {
- title: "No Title",
- sensors: [id],
- };
- sensorsToShow.push(newAsset);
- renderGraphCards();
- }
- // Add blank graph to the graph cards
- function addNewGraph() {
- const newAsset = {
- title: "No Title " + (sensorsToShow.length + 1),
- sensors: [],
- };
- selectedGraphTitle = newAsset.title;
- sensorsToShow.push(newAsset);
- selectGraph(sensorsToShow.length - 1)
- renderGraphCards();
- }
- // Add Sensor to an existing graph card
- function addSensorToExistingGraph(graphIndex, sensorId) {
- sensorsToShow[graphIndex].sensors.push(sensorId);
- renderGraphCards();
- }
- // Remove sensor from the graph sensor list
- function removeSensorFromGraph(graphIndex, sensorIndex) {
- sensorsToShow[graphIndex].sensors.splice(sensorIndex, 1);
- renderGraphCards();
- renderApiSensors(cachedFilteredSensors);
- }
- function handleEnterKeyEventForTitleEditing(event, graphIndex) {
- if (event.key === "Enter") {
- saveGraphTitle(graphIndex);
- renderApiSensors(cachedFilteredSensors, graphIndex);
- }
- }
- // ============== Flex Context Management BEGIN ============== //
- async function removeFlexContextField(fieldName) {
- assetFlexContext[fieldName] = null;
- renderFlexContextForm();
- }
- async function removeFlexContextSensorValue(fieldName, sensorId) {
- if (sensorId) {
- const fieldValue = assetFlexContext[fieldName];
- const isArray = Array.isArray(fieldValue);
- const isKeyValueObject = typeof fieldValue === "object" &&
- fieldValue !== null &&
- !Array.isArray(fieldValue) && // Exclude arrays
- Object.keys(fieldValue).length > 0;
- if (isArray) {
- const sensorIndex = assetFlexContext[fieldName].indexOf(sensorId);
- assetFlexContext[fieldName].splice(sensorIndex, 1);
- } else if (isKeyValueObject) {
- assetFlexContext[fieldName] = "";
- }
- } else {
- assetFlexContext[fieldName] = null;
- }
- renderFlexContextForm();
- }
- async function renderFlexContextInputField(fieldName) {
- senSearchResEle.innerHTML = "";
- let fieldValue = assetFlexContext[fieldName]
- let newFieldValue;
- let sensorsContent = [];
- if (fieldValue === null) {
- newFieldValue = ""
- } else if (typeof fieldValue === "object") {
- if (fieldValue["sensor"]) {
- newFieldValue = `{"sensor": ${fieldValue["sensor"]}}`
- isSensorValue = true;
- } else if (fieldValue.length > 0) {
- newFieldValue = fieldValue.join(", ")
- }
- } else if (typeof fieldValue == "string") {
- newFieldValue = fieldValue
- }
- const isString = typeof fieldValue === "string";
- const isArray = Array.isArray(fieldValue);
- const isKeyValueObject = typeof fieldValue === "object" &&
- fieldValue !== null &&
- !Array.isArray(fieldValue) && // Exclude arrays
- Object.keys(fieldValue).length > 0;
- const InputTitle = getFlexContextFieldTitle(fieldName);
- if (isKeyValueObject) {
- const sensors = [fieldValue["sensor"]]
- sensorsContent = await Promise.all(
- sensors.map(async (sensor, sensorIndex) => {
- const sensorData = await getSensor(fieldValue["sensor"]);
- const Asset = await getAsset(sensorData.generic_asset_id);
- const Account = await getAccount(Asset.account_id);
- return `
- <div class="p-1 mb-3">
- <div class="d-flex justify-content-between">
- <div>
- <b>Sensor:</b> <a href="${apiBasePath}/sensors/${sensorData.id}">${sensorData.id}</a>,
- <b>Unit:</b> ${sensorData.unit},
- <b>Name:</b> ${sensorData.name},
- <div style="padding-top: 1px;"></div>
- <b>Asset:</b> ${Asset.name},
- <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
- </div>
- </div>
- </div>`;
- }));
- } else if (isArray) {
- sensorsContent = await Promise.all(
- fieldValue.map(async (sensor, sensorIndex) => {
- const sensorData = await getSensor(sensor);
- const Asset = await getAsset(sensorData.generic_asset_id);
- const Account = await getAccount(Asset.account_id);
- return `
- <div class="p-1 mb-3">
- <div class="d-flex justify-content-between">
- <div>
- <b>Sensor:</b> <a href="${apiBasePath}/sensors/${sensorData.id}">${sensorData.id}</a>,
- <b>Unit:</b> ${sensorData.unit},
- <b>Name:</b> ${sensorData.name},
- <div style="padding-top: 1px;"></div>
- <b>Asset:</b> ${Asset.name},
- <b>Account:</b> ${Account?.name ? Account.name : "PUBLIC"}
- </div>
- <i class="fa fa-times"
- onclick="removeFlexContextSensorValue('${fieldName}', ${sensorData.id})"
- data-bs-toggle="tooltip"
- data-bs-placement="top"
- title="Remove Sensor"
- style="cursor: pointer;"
- ></i>
- </div>
- </div>`;
- }));
- }
- return `
- <div class="row g-2 my-1 card-highlight p-1 ${isString ? "bg-fixed-val" : "bg-dynamic-val"}" id="${fieldName}-control">
- <div class="mb-2 d-flex justify-content-between align-items-center pb-2">
- <label id="${fieldName}-label" for="${fieldName}-control-input" class="form-label mb-0 fs-5 fw-semi-bold">${InputTitle}</label>
- <i class="fa fa-times"
- onclick="removeFlexContextField('${fieldName}')"
- data-bs-toggle="tooltip"
- data-bs-placement="top"
- title="Unset this field"
- style="cursor: pointer;"
- ></i>
- </div>
- ${isKeyValueObject || isArray ? `
- <div>
- ${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>`}
- </div>
- ` : ""}
- ${isString ? `
- <div class="shadow-sm pb-2 px-2">
- <span class="fw-light fs-4">${newFieldValue ? newFieldValue : "<i>Not Set</i>"}</span>
- </div>
- ` : ""}
- </div>
- `;
- }
- async function renderFlexContextForm() {
- flexContextFormEle.innerHTML = "";
- for (const [key, value] of Object.entries(assetFlexContext)) {
- if (value !== null) {
- flexContextFormEle.innerHTML += await renderFlexContextInputField(key);
- }
- }
- renderFlexSelectOptions();
- selectFlexField();
- renderSelectInfoCards();
- }
- async function udpateFlexContextFieldValue(dataType, sensorId) {
- let highlightedCard = document.querySelector('.border-on-click');
- if (!highlightedCard) {
- showToast("Please select a field to update", "error");
- return;
- }
-
- // get card id and remove '-control' at the end
- const cardId = highlightedCard.id;
- const flexId = cardId.slice(0, -8);
- if (dataType === "fixedValue") {
- const powerCapacityInputValue = document.getElementById("fixed-value").value;
- // check if value is valid
- if(powerCapacityInputValue === "") {
- showToast("Please enter a value", "error");
- return;
- }
- assetFlexContext[flexId] = powerCapacityInputValue;
- } else if (dataType === "sensor" && sensorId) {
- if (flexId === "inflexible-device-sensors") {
- if (assetFlexContext[flexId]) {
- assetFlexContext[flexId].push(sensorId);
- } else {
- assetFlexContext[flexId] = [sensorId];
- }
- } else {
- assetFlexContext[flexId] = { sensor: sensorId };
- }
- }
- highlightedCard = await renderFlexContextInputField(flexId);
- flexContextFormEle.innerHTML = flexContextFormEle.innerHTML.replace(document.getElementById(`${flexId}-control`).outerHTML, highlightedCard);
- await selectFlexField(document.getElementById(`${flexId}-control`));
- // Update FlexContext
- await updateFlexContext();
- }
- async function addFlexContextField() {
- const fieldName = flexSelect.value;
- if (fieldName === "blank") {
- showToast("Please select a field to add", "error");
- return;
- }
- assetFlexContext[fieldName] = "";
- flexOptionsContainer.innerHTML = "";
- await renderFlexContextForm(); // im rendring here so te card with the 'border-on-click' class is updated/exists
- const card = document.getElementById(`${fieldName}-control`);
- selectFlexField(card);
- }
- function getFlexContextFieldTitle(fieldName){
- return fieldName.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
- }
- function renderFlexSelectOptions() {
- let assetFlexContextTemplate = {
- "consumption-price": null,
- "production-price": null,
- "site-power-capacity": null,
- "site-production-capacity": null,
- "site-consumption-capacity": null,
- "site-consumption-breach-price": null,
- "site-production-breach-price": null,
- "site-peak-consumption": null,
- "site-peak-consumption-price": null,
- "site-peak-production": null,
- "site-peak-production-price": null,
- "inflexible-device-sensors": []
- }
- // comapare the assetFlexContext with the template and get the fields that are not set
- assetFlexContextTemplate = Object.assign(assetFlexContextTemplate, assetFlexContext);
- flexSelect.innerHTML = `<option value="blank">Select an option</option>`;
- for (const [key, value] of Object.entries(assetFlexContextTemplate)) {
- if (value === null || value.length === 0) {
- flexSelect.innerHTML += `<option value="${key}">${getFlexContextFieldTitle(key)}</option>`;
- }
- }
- }
- function renderFlexInputOptions(contextKey, sensorsOnly = false) {
- return `
- <div class="card-header">
- <ul class="nav nav-tabs card-header-tabs" id="pills-tab" role="tablist">
- ${sensorsOnly ? `
- <li class="nav-item" role="presentation">
- <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">
- Dynamic value (a sensor)
- </button>
- </li>
- ` : `
- <li class="nav-item" role="presentation">
- <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">
- Fixed value
- </button>
- </li>
- <li class="nav-item" role="presentation">
- <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">
- Dynamic value (a sensor)
- </button>
- </li>
- `}
- </ul>
-
- <div class="tab-content" id="pills-tabContent">
- ${sensorsOnly ? `
- <div class="tab-pane fade show active bg-dynamic-val rounded-bottom" id="pills-dynamic" role="tabpanel" aria-labelledby="pills-dynamic-tab">
- <div id="select-sensor-input" class="flex-input-group mb-4 p-2">
- <label for="sensor" class="form-label">Look up Sensor for <b>${getFlexContextFieldTitle(contextKey)} </b>
- <i class="fa fa-info-circle ps-2"
- data-bs-toggle="tooltip"
- data-bs-placement="bottom"
- title="Search for sensor(s) to add here. Make sure that the sensor data will be available consistently, though, so scheduling will work."
- ></i>
- </label>
- <div class="form-check">
- <input class="form-check-input" type="checkbox" value="" id="flexCheckBox" onchange="searchSensorsFlexContextForm()"/>
- <label class="form-check-label" for="flexCheckBox">
- Include Public Assets
- </label>
- </div>
-
- <div class="row">
- <div class="col-8 pe-1">
- <input
- type="text"
- id="flexContextSensorSearch"
- class="form-control"
- placeholder="Search sensors..."
- oninput="searchSensorsFlexContextForm()"
- />
- </div>
- <div class="col-4 ps-0">
- <select id="flexUnitsSelect" class="form-select" onchange="searchSensorsFlexContextForm()">
- <option value="${null}" selected>Units</option>
- ${availableUnits.map( unit => '<option value="' + unit + '">' + unit + '</option>').join('')}
- </select>
- </div>
- </div>
- </div>
- </div>
- ` : `
- <div class="tab-pane fade show active bg-fixed-val rounded-bottom" id="pills-fixed" role="tabpanel" aria-labelledby="pills-fixed-tab">
- <div class="flex-input-group mb-4 p-2">
- <label for="fixed-value" class="form-label">Value for <b>${getFlexContextFieldTitle(contextKey)} </b>
- <i class="fa fa-info-circle ps-2"
- data-bs-toggle="tooltip"
- data-bs-placement="bottom"
- 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."
- ></i>
- </label>
- <input type="text" id="fixed-value" class="form-control"
- value="${typeof(assetFlexContext[contextKey]) == 'string' ? assetFlexContext[contextKey] : ""}"
- placeholder="e.g. 15000 kW"
- onkeydown="if(event.key === 'Enter') { udpateFlexContextFieldValue('fixedValue', ${null}) }"
- >
- <button class="btn btn-secondary btn-sm me-2 mt-2"
- onclick="udpateFlexContextFieldValue('fixedValue', ${null})">Set Value</button>
- </div>
- </div>
- <div class="tab-pane fade bg-dynamic-val rounded-bottom" id="pills-dynamic" role="tabpanel" aria-labelledby="pills-dynamic-tab">
- <div id="select-sensor-input" class="flex-input-group mb-4 p-2">
- <label for="sensor" class="form-label">Look up Sensor for <b>${getFlexContextFieldTitle(contextKey)} </b>
- <i class="fa fa-info-circle ps-2"
- data-bs-toggle="tooltip"
- data-bs-placement="bottom"
- 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."
- ></i>
- </label>
- <div class="form-check">
- <input class="form-check-input" type="checkbox" value="" id="flexCheckBox"/>
- <label class="form-check-label" for="flexCheckBox">
- Include Public Assets
- </label>
- </div>
- <div class="row">
- <div class="col-8 pe-1">
- <input
- type="text"
- id="flexContextSensorSearch"
- class="form-control"
- placeholder="Search sensors..."
- oninput="searchSensorsFlexContextForm()"
- />
- </div>
- <div class="col-4 ps-0">
- <select id="flexUnitsSelect" class="form-select" onchange="searchSensorsFlexContextForm()">
- <option value="${null}" selected>Units</option>
- ${availableUnits.map( unit => '<option value="' + unit + '">' + unit + '</option>').join('')}
- </select>
- </div>
- </div>
- </div>
- </div>
- `}
- </div>
- </div>
- `;
- }
- function selectFlexField(highlightedCard) {
- if (highlightedCard) {
- if (highlightedCard.classList.contains("border-on-click")) {
- // Pass
- } else {
- document.querySelectorAll(".card-highlight").forEach(el => el.classList.remove("border-on-click"));
- flexOptionsContainer.innerHTML = "";
- highlightedCard.classList.add("border-on-click");
- const cardLabel = highlightedCard.querySelector("label");
- if (cardLabel) {
- // remove "-label" at the end leaing the flexcontext key
- const flexKey = cardLabel.id.slice(0, -6);
- handleFlexSelectChange(flexKey);
- if (flexKey === "inflexible-device-sensors" || flexKey === "consumption-price" || flexKey === "production-price") {
- flexOptionsContainer.innerHTML = renderFlexInputOptions(flexKey, true);
- } else {
- flexOptionsContainer.innerHTML = renderFlexInputOptions(flexKey);
- }
- }
- }
- } else {
- document.querySelectorAll(".card-highlight").forEach(el => el.classList.remove("border-on-click"));
- flexOptionsContainer.innerHTML = "";
- }
- }
- // ============== Flex Context Management END ============== //
- // ============== Page Events ============== //
- flexContexFormModal.addEventListener('shown.bs.modal', function () {
- // Initial renders
- renderFlexContextForm(); // Initial render of flex context form
- });
- flexContexFormModal.addEventListener('hidden.bs.modal', function () {
- updateFlexContext();
- });
- document.addEventListener("click", function (event) {
- /**
- The logic in this block is majorly to remove the border on click of the card and add it to the selected card
- but as this event is added to the document, it will be triggered on any click event on the page
- so the if statements are used to check if the click event is on the card or not
- */
- const card = event.target.closest(".card-highlight");
- const sensorCard = event.target.closest(".sensor-card");
- const searchInput = event.target.id === "searchInput";
- const cardBody = event.target.closest(".card-body");
- const editTitleBtn = event.target.id === "editTitleBtn";
- const saveTitleBtn = event.target.id === "saveTitleBtn";
- const unSetBtn = event.target.id === "unSetFlexField";
- if (card) {
- if (card.classList.contains("border-on-click")) {
- if (editTitleBtn || saveTitleBtn) {
- renderGraphCards();
- }
- // Pass
- } else {
- selectFlexField(card)
- renderGraphCards();
- }
- } else if (
- cardBody !== null && cardBody !== undefined ||
- sensorCard !== null && sensorCard !== undefined ||
- searchInput !== null && searchInput !== undefined
- ) {
- // Pass
- } else {
- document.querySelectorAll(".card-highlight").forEach(el => el.classList.remove("border-on-click"));
- renderApiSensors([], undefined);
- }
- });
- function renderSelectInfoCards(contextKey, description, allowedUnits, isArray = false) {
- if (contextKey) {
- return `
- <div>
- <div class="alert alert-info" role="alert">
- <b> About ${getFlexContextFieldTitle(contextKey)}
- <div class="pt-2 fw-normal">
- ${description}
- </div>
- <div class="pt-3 fw-bold">
- Example Units: <span class="fw-normal"> ${allowedUnits.join(", ")} </span>
- </div>
-
- </div>
- </div>
- `;
- } else {
- flexInfoContainer.innerHTML = "";
- }
- }
- function handleFlexSelectChange(selectedOption) {
- flexInfoContainer.innerHTML = "";
- const alertStyle = "alert alert-light";
- if (selectedOption === "consumption-price") {
- const description = "Set the sensor that represents the consumption price of the site. This value will be used in the optimization";
- const allowedUnits = ["EUR/MWh", "JPY/kWh", "USD/MWh, and other currencies."];
- flexInfoContainer.innerHTML = renderSelectInfoCards("consumption-price", description, allowedUnits);
- } else if (selectedOption === "production-price") {
- const description = "Set the sensor that represents the production price of the site. This value will be used in the optimization";
- const allowedUnits = ["EUR/MWh", "JPY/kWh", "USD/MWh, and other currencies."];
- flexInfoContainer.innerHTML = renderSelectInfoCards("production-price", description, allowedUnits);
- } else if (selectedOption == "site-power-capacity") {
- const description = "This value represents the maximum power that the site can consume or produce. This value will be used in the optimization";
- const allowedUnits = ["kW", "kVA", "MVA"];
- flexInfoContainer.innerHTML = renderSelectInfoCards("site-power-capacity", description, allowedUnits);
- } else if (selectedOption == "site-production-capacity") {
- const description = "This value represents the maximum power that the site can produce. This value will be used in the optimization";
- const allowedUnits = ["kW"];
- flexInfoContainer.innerHTML = renderSelectInfoCards("site-production-capacity", description, allowedUnits);
- } else if (selectedOption == "site-consumption-capacity") {
- const description = "This value represents the maximum power that the site can consume. This value will be used in the optimization";
- const allowedUnits = ["kW"];
- flexInfoContainer.innerHTML = renderSelectInfoCards("site-consumption-capacity", description, allowedUnits);
- } else if (selectedOption == "site-consumption-breach-price") {
- 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";
- const allowedUnits = ["EUR/MW", "JPY/kW", "USD/MW, and other currencies."];
- flexInfoContainer.innerHTML = renderSelectInfoCards("site-consumption-breach-price", description, allowedUnits);
- } else if (selectedOption == "site-production-breach-price") {
- 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";
- const allowedUnits = ["EUR/MW", "JPY/kW", "USD/MW, and other currencies."];
- flexInfoContainer.innerHTML = renderSelectInfoCards("site-production-breach-price", description, allowedUnits);
- } else if (selectedOption == "site-peak-consumption") {
- const description = "This value represents the peak consumption of the site. This value will be used in the optimization";
- const allowedUnits = ["kW"];
- flexInfoContainer.innerHTML = renderSelectInfoCards("site-peak-consumption", description, allowedUnits);
- } else if (selectedOption == "site-peak-production") {
- const description = "This value represents the peak production of the site. This value will be used in the optimization";
- const allowedUnits = ["kW"];
- flexInfoContainer.innerHTML = renderSelectInfoCards("site-peak-production", description, allowedUnits);
- } else if (selectedOption == "site-peak-consumption-price") {
- 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";
- const allowedUnits = ["EUR/MW", "JPY/kW", "USD/MW, and other currencies."];
- flexInfoContainer.innerHTML = renderSelectInfoCards("site-peak-consumption-price", description, allowedUnits);
- } else if (selectedOption == "site-peak-production-price") {
- 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";
- const allowedUnits = ["EUR/MW", "JPY/kW", "USD/MW, and other currencies."];
- flexInfoContainer.innerHTML = renderSelectInfoCards("site-peak-production-price", description, allowedUnits);
- } else if (selectedOption == "inflexible-device-sensors") {
- const description = "This value represents the sensors that are inflexible and cannot be controlled. These sensors will be used in the optimization";
- const allowedUnits = ["kW"];
- flexInfoContainer.innerHTML = renderSelectInfoCards("inflexible-device-sensors", description, allowedUnits);
- }
- }
- flexSelect.addEventListener('change', function () {
- handleFlexSelectChange(flexSelect.value);
- });
- // ============== Page Events ============== //
- </script>
- {% endblock %}
|