UNPKG

pareto-anywhere

Version:

Open source IoT middleware suite that makes the data from just about anything usable. We believe in an open Internet of Things.

295 lines (248 loc) 9.89 kB
/** * Copyright reelyActive 2022-2024 * We believe in an open Internet of Things */ // Constants const DYNAMB_ROUTE = '/devices/dynamb'; const SIGNATURE_SEPARATOR = '/'; const TIME_OPTIONS = { hour: "2-digit", minute: "2-digit", hour12: false }; const DEMO_SEARCH_PARAMETER = 'demo'; // DOM elements let connectIcon = document.querySelector('#connectIcon'); let demoalert = document.querySelector('#demoalert'); let occupancytable = document.querySelector('#occupancytable'); let chairsoccupied = document.querySelector('#chairsoccupied'); let chairsavailable = document.querySelector('#chairsavailable'); let desksoccupied = document.querySelector('#desksoccupied'); let desksavailable = document.querySelector('#desksavailable'); let roomsoccupied = document.querySelector('#roomsoccupied'); let roomsavailable = document.querySelector('#roomsavailable'); let time = document.querySelector('#time'); // Other variables let occupancyCompilation = new Map(); let assetStatus = { chairsOccupied: 0, chairsAvailable: 0, desksoccupied: 0, desksAvailable: 0, roomsOccupied: 0, roomsAvailable: 0 }; let baseUrl = window.location.protocol + '//' + window.location.hostname + ':' + window.location.port; // Initialise based on URL search parameters, if any let searchParams = new URLSearchParams(location.search); let isDemo = searchParams.has(DEMO_SEARCH_PARAMETER); // Demo mode: connect to starling.js if(isDemo) { let demoIcon = createElement('b', 'animate-breathing text-success', 'DEMO'); connectIcon.replaceChildren(demoIcon); starling.on("dynamb", handleDynamb); } // Normal mode: connect to socket.io else { let socket = io(baseUrl + DYNAMB_ROUTE); socket.on("dynamb", handleDynamb); // Display changes to the socket.io connection status socket.on("connect", function() { connectIcon.replaceChildren(createElement('i', 'fas fa-cloud text-success')); demoalert.hidden = true; }); socket.on("connect_error", function() { connectIcon.replaceChildren(createElement('i', 'fas fa-cloud text-danger')); demoalert.hidden = false; }); socket.on("disconnect", function(reason) { connectIcon.replaceChildren(createElement('i', 'fas fa-cloud text-warning')); }); } // Begin periodic updates of stats display update(); // Handle a dynamb event function handleDynamb(dynamb) { let deviceSignature = dynamb.deviceId + SIGNATURE_SEPARATOR + dynamb.deviceIdType; if(dynamb.hasOwnProperty('isMotionDetected')) { let status = occupancyCompilation.get(deviceSignature); let isMotionDetected = dynamb.isMotionDetected.includes(true); if(status) { status.current = isMotionDetected; } else { status = { current: isMotionDetected, previous: [ null, null, null ], tags: [] }; if(isDemo) { let tags = [ 'chair', 'desk', 'room' ]; status.tags.push(tags[Math.floor(Math.random() * tags.length)]); } else { retrieveMetadata(deviceSignature); } } updateOccupancyRow(status, deviceSignature); occupancyCompilation.set(deviceSignature, status); } } // Retrieve the associations and story, if any, for the given device function retrieveMetadata(deviceSignature) { cormorant.retrieveAssociations(baseUrl, deviceSignature, { isStoryToBeRetrieved: true }, (associations, story, status) => { if(associations) { let status = occupancyCompilation.get(deviceSignature) || { current: null, previous: [ null, null, null ], tags: [] }; if(Array.isArray(associations.tags)) { if(associations.tags.includes('chair')) { status.tags.push('chair'); } if(associations.tags.includes('desk')) { status.tags.push('desk'); } if(associations.tags.includes('room')) { status.tags.push('room'); } } if(story) { status.title = cuttlefishStory.determineTitle(story) || deviceSignature; } occupancyCompilation.set(deviceSignature, status); } }); } // Compile statistics, update time and display function update() { time.textContent = new Date().toLocaleTimeString([], TIME_OPTIONS); updateCompilation(); updateDisplay(); let millisecondsToNextMinute = 60000 - (Date.now() % 60000); setTimeout(update, millisecondsToNextMinute); } // Update isMotionDetected samples and chair/desk/room stats function updateCompilation() { assetStatus.chairsOccupied = 0; assetStatus.chairsAvailable = 0; assetStatus.desksOccupied = 0; assetStatus.desksAvailable = 0; assetStatus.roomsOccupied = 0; assetStatus.roomsAvailable = 0; occupancyCompilation.forEach((status, deviceSignature) => { if(status.current === true) { if(status.tags.includes('chair')) { assetStatus.chairsOccupied++; } if(status.tags.includes('desk')) { assetStatus.desksOccupied++; } if(status.tags.includes('room')) { assetStatus.roomsOccupied++; } } else if(status.current === false) { if(status.tags.includes('chair')) { assetStatus.chairsAvailable++; } if(status.tags.includes('desk')) { assetStatus.desksAvailable++; } if(status.tags.includes('room')) { assetStatus.roomsAvailable++; } } status.previous.pop(); status.previous.unshift(status.current); status.current = null; }); } // Update the compilation display function updateDisplay() { occupancyCompilation.forEach(updateOccupancyRow); let totalChairs = assetStatus.chairsOccupied + assetStatus.chairsAvailable; let totalDesks = assetStatus.desksOccupied + assetStatus.desksAvailable; let totalRooms = assetStatus.roomsOccupied + assetStatus.roomsAvailable; if(totalChairs === 0) { chairsoccupied.setAttribute('style', 'width:0%;'); chairsavailable.setAttribute('style', 'width:0%;'); } else { let occupied = Math.round(100 * assetStatus.chairsOccupied / totalChairs); let available = Math.round(100 * assetStatus.chairsAvailable / totalChairs); chairsoccupied.setAttribute('style', 'width:' + occupied + '%;'); chairsavailable.setAttribute('style', 'width:' + available + '%;'); } chairsoccupied.textContent = assetStatus.chairsOccupied; chairsavailable.textContent = assetStatus.chairsAvailable; if(totalDesks === 0) { desksoccupied.setAttribute('style', 'width:0%;'); desksavailable.setAttribute('style', 'width:0%;'); } else { let occupied = Math.round(100 * assetStatus.desksOccupied / totalDesks); let available = Math.round(100 * assetStatus.desksAvailable / totalDesks); desksoccupied.setAttribute('style', 'width:' + occupied + '%;'); desksavailable.setAttribute('style', 'width:' + available + '%;'); } desksoccupied.textContent = assetStatus.desksOccupied; desksavailable.textContent = assetStatus.desksAvailable; if(totalRooms === 0) { roomsoccupied.setAttribute('style', 'width:0%;'); roomsavailable.setAttribute('style', 'width:0%;'); } else { let occupied = Math.round(100 * assetStatus.roomsOccupied / totalRooms); let available = Math.round(100 * assetStatus.roomsAvailable / totalRooms); roomsoccupied.setAttribute('style', 'width:' + occupied + '%;'); roomsavailable.setAttribute('style', 'width:' + available + '%;'); } roomsoccupied.textContent = assetStatus.roomsOccupied; roomsavailable.textContent = assetStatus.roomsAvailable; } // Update (or create) a single occupancy row function updateOccupancyRow(status, deviceSignature) { let tr = document.getElementById(deviceSignature); if(tr) { let tds = tr.getElementsByTagName('td'); if(status.title) { tds[0].replaceChildren(status.title); } if(status.current !== null) { tds[1].replaceChildren(createOccupancyIcon(status.current)); } else { tds[1].replaceChildren(); } tds[2].replaceChildren(createOccupancyIcon(status.previous[0])); tds[3].replaceChildren(createOccupancyIcon(status.previous[1])); tds[4].replaceChildren(createOccupancyIcon(status.previous[2])); } else { let tds = [ createElement('td', null, status.title || deviceSignature), createElement('td', 'animate-breathing', createOccupancyIcon(status.current)), createElement('td', 'bg-body-secondary', createOccupancyIcon(status.previous[0])), createElement('td', null, createOccupancyIcon(status.previous[1])), createElement('td', null, createOccupancyIcon(status.previous[2])) ]; tr = createElement('tr', null, tds); tr.setAttribute('id', deviceSignature); occupancytable.appendChild(tr); } } // Create an icon depending on whether true, false or null function createOccupancyIcon(isMotionDetected) { let iconClass; switch(isMotionDetected) { case true: iconClass = 'fas fa-walking text-secondary'; break; case false: iconClass = 'fas fa-times-circle text-success'; break; default: iconClass = 'fas fa-question-circle text-body-tertiary'; } return createElement('i', iconClass); } // Create an element as specified function createElement(elementName, classNames, content) { let element = document.createElement(elementName); if(classNames) { element.setAttribute('class', classNames); } if((content instanceof Element) || (content instanceof DocumentFragment)) { element.appendChild(content); } else if(Array.isArray(content)) { content.forEach(function(item) { if((item instanceof Element) || (item instanceof DocumentFragment)) { element.appendChild(item); } else { element.appendChild(document.createTextNode(item)); } }); } else if(content) { element.appendChild(document.createTextNode(content)); } return element; }