Last active
February 23, 2019 15:09
-
-
Save iiic/f932a9861af5d688be3bcd897ad030f4 to your computer and use it in GitHub Desktop.
iotic project
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="cs" dir="ltr"> | |
<head> | |
<link href="iotic.js" rel="preload" as="script"> | |
<meta charset="utf-8"> | |
<title>iotic</title> | |
<!-- <link href="…" rel="shortcut icon"> --> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<meta name="referrer" content="origin-when-crossorigin"> | |
<style> | |
body { | |
background: #fff; | |
} | |
hr { | |
display: none; | |
} | |
canvas { | |
margin-bottom: 5em; | |
} | |
main { | |
margin-right: 22rem; | |
} | |
aside { | |
width: 22rem; | |
position: fixed; | |
right: 0; | |
top: 0; | |
} | |
</style> | |
<meta name="theme-color" content="#dee7f8"> | |
<link Xhref="manifest.json" rel="manifest"> | |
<link type="text/plain" rel="author" href="humans.txt"> | |
</head> | |
<body> | |
<header> | |
hlavička | |
</header> | |
<hr> | |
<main id="index" role="main"> | |
<section id="rootCanvas"> | |
<h2>Automatické grafy</h2> | |
</section> | |
</main> | |
<hr> | |
<aside id="dashboard" role="complementary"> | |
<h3>Nějaký hodně zajímavý informace</h3> | |
<p>Zdroj dat: <span id="ws-status"></span></p> | |
<p>Součet měření: <span id="n-hits"></span></p> | |
</aside> | |
<hr> | |
<footer role="contentinfo"> | |
patička | |
<a href="#index" id="scroll-to-top" class="mdl-button" hidden><i class="fa fa-caret-up" aria-hidden="true">dostaň mě nahorů</i></a> | |
</footer> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.3/Chart.min.js"></script> | |
<script src="https://rawgit.com/chartjs/chartjs-plugin-annotation/master/chartjs-plugin-annotation.js"></script> | |
<script src="iotic.js"></script> | |
<script> | |
const a = new iotic( window ); | |
//a.settings = {'lang': 'cs', '…': true}; | |
a.run(); | |
</script> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
'use strict'; | |
/** @typedef {function(): true} Chart - global Chart export from library Chart.js */ | |
var Chart; | |
const iotic = class | |
{ | |
/* | |
* @constructor | |
* @param {Window} w - root website object | |
*/ | |
constructor ( w = window ) | |
{ | |
/* | |
* @public | |
*/ | |
this.window = w; | |
this.idb = this.window.indexedDB | |
|| this.window[ 'mozIndexedDB' ] | |
|| this.window[ 'webkitIndexedDB' ] | |
|| this.window[ 'msIndexedDB' ] | |
|| this.window[ 'shimIndexedDB' ]; // This works on all sensors/browsers, and uses IndexedDBShim as a final fallback | |
/* | |
* @private | |
*/ | |
this._settings = { | |
wsSource: new URL( '/ws/iot', 'ws://localhost:1880' ), | |
db_credentials: { | |
db_name: 'IOT', | |
table_name: 'hits', | |
version: 1, | |
}, | |
rootCanvasElement: document.getElementById( 'rootCanvas' ), | |
unitsByMeasurement: { | |
voltage: 'volts', | |
}, | |
graphs: { | |
divider: ',', | |
knownColors: { | |
}, | |
special: { | |
'co2-meter': { | |
concentration: { | |
systematicError: { | |
fixed: 50, // in units | |
variable: 0.03, // in % … between 0 and 1 | |
}, | |
options: { | |
tooltipsTextPrefix: 'co\u2082 concentration: ', // SUBSCRIPT TWO | |
}, | |
}, | |
}, | |
barometer: { | |
pressure: { | |
convertValues: 1000, // x times less | |
systematicError: { | |
fixed: 400, // in units | |
variable: 0, // in % … between 0 and 1 | |
}, | |
measurementRange: { | |
from: 20000, | |
to: 110000, | |
}, | |
options: { | |
tooltipsTextPrefix: 'barometer pressure: ', | |
}, | |
}, | |
}, | |
}, | |
}, | |
statistics: { | |
nHitsElement: document.getElementById( 'n-hits' ), | |
wsStatus: document.getElementById( 'ws-status' ), | |
}, | |
units: { | |
'co2-meter': { | |
concentration: { | |
abbr: 'ppm', | |
longName: 'Parts per million', | |
}, | |
}, | |
barometer: { | |
pressure: { | |
abbr: 'kPa', | |
longName: 'kilo Pascal', | |
}, | |
} | |
}, | |
systematicErrorText: 'soustavná chyba', | |
}; | |
/* | |
* @private | |
*/ | |
this._ws = {}; | |
/* | |
* @private | |
*/ | |
this._chartReferences = {}; | |
/** | |
* @private | |
*/ | |
this._CHART_COLORS = Object.freeze( { // from pallete https://www.materialui.co/colors | |
lightness300: { | |
red: '#e57373', | |
pink: '#f06292', | |
purple: '#ba68c8', | |
deepPurple: '#9575cd', | |
indigo: '#7986cb', | |
blue: '#64b5f6', | |
lightBlue: '#4fc3f7', | |
cyan: '#4dd0e1', | |
teal: '#4db6ac', | |
green: '#81c784', | |
lightGreen: '#aed581', | |
lime: '#dce775', | |
yellow: '#fff176', | |
amber: '#ffd54f', | |
orange: '#ffb74d', | |
deepOrange: '#ff8a65', | |
brown: '#a1887f', | |
grey: '#e0e0e0', | |
blueGrey: '#90a4ae', | |
}, | |
lightness400: { | |
red: '#ef5350', | |
pink: '#ec407a', | |
purple: '#ab47bc', | |
deepPurple: '#7e57c2', | |
indigo: '#5c6bC0', | |
blue: '#42a5f5', | |
lightBlue: '#29b6f6', | |
cyan: '#26c6da', | |
teal: '#26a69a', | |
green: '#66bb6a', | |
lightGreen: '#9ccc65', | |
lime: '#d4e157', | |
yellow: '#ffee58', | |
amber: '#ffca28', | |
orange: '#ffa726', | |
deepOrange: '#ff7043', | |
brown: '#8d6e63', | |
grey: '#bdbdbd', | |
blueGrey: '#78909c', | |
}, | |
} ); | |
this.hashCode = Symbol( 'ic::String' ); | |
String.prototype[ this.hashCode ] = function () // collision probability is 31^11 | |
{ | |
let hash = 0; | |
for ( let i = 0; i < this.length; i++ ) { | |
const character = this.charCodeAt( i ); | |
hash = ( ( hash << 5 ) - hash ) + character; | |
hash = hash & hash; // Convert to 32bit integer | |
} | |
return hash; | |
}; | |
this.seconds2Hms = Symbol( 'ic::Number' ); | |
Number.prototype[ this.seconds2Hms ] = function ( /** @type {String=} */ lang = 'en' ) | |
{ | |
const HOURS_SEPARATOR = ( lang === 'cs' ? '.' : ':' ); | |
const MINUTES_SEPARATOR = ':'; | |
const h = Math.floor( Number( this ) / 3600 ); | |
const m = Math.floor( Number( this ) % 3600 / 60 ); | |
const s = Math.floor( Number( this ) % 3600 % 60 ); | |
const hString = ( h > 0 ) ? ( h <= 9 ? '0' + h : h ) : '00'; | |
const mString = ( m > 0 ) ? ( m <= 9 ? '0' + m : m ) : '00'; | |
const sString = ( s > 0 ) ? ( s <= 9 ? '0' + s : s ) : '00'; | |
return hString + HOURS_SEPARATOR + mString + MINUTES_SEPARATOR + sString; | |
}; | |
} | |
/** | |
* Set settings for whole class | |
*/ | |
set settings ( variables ) | |
{ | |
level1: // eslint-disable-line no-unused-labels | |
for ( const i in variables ) { | |
if ( typeof variables[ i ] === 'object' | |
&& variables[ i ].constructor.name === 'Object' | |
&& typeof this._settings[ i ] !== 'undefined' | |
) { | |
level2: // eslint-disable-line no-unused-labels | |
for ( const ii in variables[ i ] ) { | |
if ( typeof variables[ i ][ ii ] === 'object' && variables[ i ][ ii ].constructor.name === 'Object' ) { | |
level3: // eslint-disable-line no-unused-labels | |
for ( const iii in variables[ i ][ ii ] ) { | |
if ( typeof variables[ i ][ ii ][ iii ] === 'object' && variables[ i ][ ii ][ iii ].constructor.name === 'Object' ) { | |
delete variables[ i ][ ii ][ iii ]; | |
} | |
} | |
Object.assign( this._settings[ i ][ ii ], variables[ i ][ ii ] ); | |
delete variables[ i ][ ii ]; | |
} | |
} | |
Object.assign( this._settings[ i ], variables[ i ] ); | |
delete variables[ i ]; | |
} | |
} | |
Object.assign( this._settings, variables ); | |
} | |
/* | |
* Get settings for whole class | |
* @returns {Object} | |
*/ | |
get settings () | |
{ | |
return this._settings; | |
} | |
set ws ( inObj ) | |
{ | |
this._ws = inObj; | |
} | |
/* | |
* @todo : description | |
* @returns {Object} | |
*/ | |
get ws () | |
{ | |
return this._ws; | |
} | |
set chartReferences ( inObj ) | |
{ | |
this._chartReferences = inObj; | |
} | |
/* | |
* @todo : description | |
* @returns {Object} | |
*/ | |
get chartReferences () | |
{ | |
return this._chartReferences; | |
} | |
/** | |
* Get CHART_COLORS | |
*/ | |
get CHART_COLORS () | |
{ | |
return this._CHART_COLORS; | |
} | |
db_init ( /** @type {IDBOpenDBRequest} */ openReq ) | |
{ | |
/** @type {IDBVersionChangeEvent} */ | |
const event = arguments[ 1 ]; | |
/** @type {IDBDatabase} */ | |
const db = openReq.result; | |
if ( event.oldVersion > 0 && event.oldVersion < event.newVersion ) { | |
db.deleteObjectStore( this.settings.db_credentials.table_name ); | |
} | |
const store = db.createObjectStore( this.settings.db_credentials.table_name, { keyPath: 'id', autoIncrement: true } ); | |
store.createIndex( 'timestamp', 'timestamp', { unique: false } ); | |
store.createIndex( 'date', 'date', { unique: false } ); | |
store.createIndex( 'device', 'device', { unique: false } ); | |
store.createIndex( 'deviceNo', 'deviceNo', { unique: false } ); | |
store.createIndex( 'sensor', 'sensor', { unique: false } ); | |
store.createIndex( 'measurement', 'measurement', { unique: false } ); | |
store.createIndex( 'value', 'value', { unique: false } ); | |
store.createIndex( 'notice', 'notice', { unique: false } ); | |
} | |
/** | |
* @todo : description | |
*/ | |
prepareIndexedDB () | |
{ | |
/** @type {IDBOpenDBRequest} */ | |
const openReq = this.idb.open( this.settings.db_credentials.db_name, this.settings.db_credentials.version ); | |
openReq.onupgradeneeded = this.db_init.bind( this, openReq ); | |
} | |
/** | |
* @todo : description | |
* @returns {Boolean} | |
*/ | |
storeHitToIndexedDB ( hit = {} ) | |
{ | |
console.log( 'storeHitToIndexedDB', hit ); | |
/** @type {IDBOpenDBRequest} */ | |
const openReq = this.idb.open( this.settings.db_credentials.db_name, this.settings.db_credentials.version ); | |
openReq.onupgradeneeded = this.db_init.bind( this, openReq ); | |
openReq.onsuccess = function () | |
{ | |
/** @type {IDBDatabase} */ | |
const db = openReq.result; | |
const tx = db.transaction( this.settings.db_credentials.table_name, 'readwrite' ); | |
const store = tx.objectStore( this.settings.db_credentials.table_name ); | |
store.add( { | |
//id: 'is created automatically by autoIncrement' | |
timestamp: hit.timestamp, | |
date: + new Date, // + triggers .valueOf() | |
device: hit.device, | |
deviceNo: hit.deviceNo, | |
sensor: hit.sensor, | |
measurement: hit.measurement, | |
value: hit.value, | |
} ); | |
tx.oncomplete = () => | |
{ | |
db.close(); | |
}; | |
}.bind( this ); | |
return true; | |
} | |
addUnitsBy ( /** @type {String} */ elementId, /** @type {Boolean} */ longName = false ) | |
{ | |
const names = this.getSpecialSettingsBy( elementId ); | |
if ( names.units ) { | |
return names.units[ longName ? 'longName' : 'abbr' ]; | |
} | |
return 'value'; // default text | |
} | |
createGraphAreasBy ( /** @this {Array} */ sensors ) | |
{ | |
for ( const name in sensors ) { | |
const measurements = [ ...sensors[ name ] ]; | |
const id = 'sensor-' + name; | |
let sensorElement = document.getElementById( id ); | |
if ( !sensorElement ) { | |
sensorElement = document.createElement( 'fieldset' ); | |
sensorElement.id = id; | |
const headTitle = document.createElement( 'legend' ); | |
headTitle.appendChild( document.createTextNode( name ) ); | |
sensorElement.appendChild( headTitle ); | |
} | |
for ( const i in measurements ) { | |
const id = this.getGraphIdFrom( name, measurements[ i ] ); | |
if ( !document.getElementById( id ) ) { | |
const measurementElement = document.createElement( 'div' ); | |
measurementElement.id = id; | |
const measurementTitle = document.createElement( 'strong' ); | |
measurementTitle.appendChild( document.createTextNode( measurements[ i ] ) ); | |
measurementElement.appendChild( measurementTitle ); | |
const canvasElement = document.createElement( 'div' ); | |
canvasElement.id = 'canvas-' + measurementElement.id; | |
sensorElement.appendChild( measurementElement ); | |
} | |
} | |
this.settings.rootCanvasElement.appendChild( sensorElement ); | |
} | |
return true; | |
} | |
/** | |
* @todo : description | |
* @async | |
* @returns {Promise<Number>} | |
*/ | |
async countHits () | |
{ | |
return new Promise( ( /** @type {Function} */ resolve ) => | |
{ | |
const openReq = this.idb.open( this.settings.db_credentials.db_name, this.settings.db_credentials.version ); | |
openReq.onsuccess = function () | |
{ | |
const db = openReq.result; | |
const transaction = db.transaction( 'hits', 'readonly' ); | |
const objectStore = transaction.objectStore( 'hits' ); | |
const countRequest = objectStore.count(); | |
countRequest.onsuccess = function () | |
{ | |
resolve( Number( countRequest.result ) ); | |
} | |
transaction.oncomplete = function () | |
{ | |
db.close(); | |
}; | |
} | |
} ); | |
} | |
/** | |
* @todo : description | |
* @async | |
* @returns {Promise<Array>} | |
*/ | |
async getSensors () | |
{ | |
return new Promise( ( resolve ) => | |
{ | |
const openReq = this.idb.open( this.settings.db_credentials.db_name, this.settings.db_credentials.version ); | |
openReq.onsuccess = function () | |
{ | |
const db = openReq.result; | |
const transaction = db.transaction( 'hits', 'readonly' ); | |
const objectStore = transaction.objectStore( 'hits' ); | |
const index = objectStore.index( 'sensor' ); | |
const openCursorRequest = index.openCursor( null, 'next' ); | |
const results = []; | |
openCursorRequest.onsuccess = function ( event ) | |
{ | |
const cursor = event.target.result; | |
if ( cursor ) { | |
results[ cursor.key ] = results[ cursor.key ] || new Set(); // initialize or add | |
results[ cursor.key ].add( cursor.value.measurement ); | |
cursor.continue(); | |
} | |
}; | |
transaction.oncomplete = function () | |
{ | |
resolve( results ); | |
db.close(); | |
}.bind( this ); | |
}.bind( this ); | |
} ); | |
} | |
/** | |
* @todo : description | |
* @async | |
* @returns {Promise<Object>} | |
*/ | |
async getAllGraphValues ( sensors ) | |
{ | |
return new Promise( ( resolve ) => | |
{ | |
const nSensors = Object.keys( sensors ).length; | |
const openReq = this.idb.open( this.settings.db_credentials.db_name, this.settings.db_credentials.version ); | |
openReq.onsuccess = function () | |
{ | |
const db = openReq.result; | |
const results = {}; | |
let n = 0; | |
for ( const i in sensors ) { | |
const transaction = db.transaction( 'hits', 'readonly' ); | |
const objectStore = transaction.objectStore( 'hits' ); | |
const index = objectStore.index( 'sensor' ); | |
const openCursorRequest = index.openCursor( IDBKeyRange.only( i ), 'next' ); | |
openCursorRequest.onsuccess = function ( event ) | |
{ | |
const cursor = event.target.result; | |
if ( cursor ) { | |
const id = this.getGraphIdFrom( i, cursor.value.measurement ); | |
const device = cursor.value.device + ':' + cursor.value.deviceNo; | |
results[ id ] = results[ id ] || {}; // initialize or add | |
results[ id ][ device ] = results[ id ][ device ] || {}; // initialize or add | |
results[ id ][ device ][ cursor.primaryKey ] = { | |
'date': cursor.value.date, | |
'value': cursor.value.value, | |
}; | |
cursor.continue(); | |
} | |
}.bind( this ); | |
transaction.oncomplete = function () | |
{ | |
n++; | |
if ( n === nSensors ) { | |
resolve( results ); | |
} | |
db.close(); | |
}.bind( this ); | |
} | |
}.bind( this ); | |
} ); | |
} | |
async createColorsFrom ( /** @type {Object} */ datasets ) | |
{ | |
return new Promise( ( /** @type {Function} */ resolve ) => | |
{ | |
const color = this.CHART_COLORS.lightness400; | |
const base = ( Object.keys( color ).length < 36 ) ? Object.keys( color ).length : 36; | |
const namesInOrder = []; | |
for ( const i in datasets ) { | |
namesInOrder.push( datasets[ i ].label ); | |
} | |
const colorsInOrder = []; | |
for ( const i in namesInOrder ) { | |
/** @type {String} */ | |
const name = namesInOrder[ i ]; | |
if ( name.split( this.settings.systematicErrorText ).length !== 1 ) { | |
colorsInOrder.push( '#eee' ); | |
continue; | |
} | |
const knownColors = this.settings.graphs.knownColors; | |
if ( Object.keys( knownColors ).includes( name ) ) { // known colors | |
colorsInOrder[ i ] = color[ knownColors[ name ] ]; | |
} else { // pseudo-random colors by names | |
const nameHash = name[ this.hashCode ]().toString( base ); | |
let firstInDecimal = ( nameHash[ 0 ] === '-' ) ? parseInt( nameHash[ 1 ], base ) : parseInt( nameHash[ 0 ], base ); | |
if ( colorsInOrder.includes( colorsInOrder[ i ] ) ) { | |
while ( colorsInOrder.includes( colorsInOrder[ i ] ) ) { | |
const n = ( firstInDecimal < base ) ? firstInDecimal++ : firstInDecimal--; | |
colorsInOrder[ i ] = color[ Object.keys( color )[ n ] ]; | |
} | |
} else { | |
colorsInOrder[ i ] = color[ Object.keys( color )[ firstInDecimal ] ]; | |
} | |
} | |
} | |
resolve( colorsInOrder ); | |
} ); | |
} | |
getGraphIdFrom (/** @type {String} */ sensorName, /** @type {String} */ measurementName ) | |
{ | |
return 'sensor-' + sensorName + '-measurement-' + measurementName; | |
} | |
addDatasetsBy ( /** @type {String} */ measurementName, /** @type {Array} */ datasets, /** @type {Object} */ specialGraphSettings ) // @todo : make it asynchronous | |
{ | |
const hex2rgba = ( hex, alpha = 1 ) => | |
{ // @todo : to custom string prototype | |
const regexpString = ( hex.length > 4 ) ? '\\w\\w' : '\\w'; | |
const [ r, g, b ] = hex.match( new RegExp( regexpString, 'g' ) ).map( x => parseInt( x, 16 ) ); | |
return `rgba(${ r },${ g },${ b },${ alpha })`; | |
}; | |
const errorAreas = []; | |
for ( let i in datasets ) { | |
const dataset = datasets[ i ]; | |
console.log( dataset ); | |
const deviationLevelMin = this.createNewDatasetWith( 'minimální ' + this.settings.systematicErrorText + ': ' + dataset.label ); | |
deviationLevelMin.fill = '+1'; | |
deviationLevelMin.hidden = true; | |
deviationLevelMin.pointRadius = 1; | |
deviationLevelMin.pointHoverRadius = 2; | |
deviationLevelMin.backgroundColor = '#eee'; // @todo : use color based on parent colors | |
deviationLevelMin.borderColor = '#eee'; // @todo : use color based on parent colors | |
const deviationLevelMax = this.createNewDatasetWith( 'maximální ' + this.settings.systematicErrorText + ': ' + dataset.label ); | |
deviationLevelMax.fill = '-1'; | |
deviationLevelMax.hidden = true; | |
deviationLevelMax.pointRadius = 1; | |
deviationLevelMax.pointHoverRadius = 2; | |
deviationLevelMax.backgroundColor = '#eee'; // @todo : use color based on parent colors | |
deviationLevelMax.borderColor = '#eee'; // @todo : use color based on parent colors | |
for ( const ref in dataset.data ) { | |
deviationLevelMin.data[ ref ] = { | |
x: dataset.data[ ref ].x, | |
y: ( dataset.data[ ref ].y * ( 1 - specialGraphSettings.systematicError.variable ) ) - specialGraphSettings.systematicError.fixed, | |
}; | |
deviationLevelMax.data[ ref ] = { | |
x: dataset.data[ ref ].x, | |
y: ( dataset.data[ ref ].y * ( 1 + specialGraphSettings.systematicError.variable ) ) + specialGraphSettings.systematicError.fixed, | |
}; | |
} | |
errorAreas.push( deviationLevelMin ); | |
errorAreas.push( deviationLevelMax ); | |
} | |
return datasets.concat( errorAreas ); | |
} | |
createNewDatasetWith ( /** @type {String} */ label ) | |
{ | |
return { | |
label: label, | |
data: [], | |
/** @type {Number | Boolean | String} */ | |
fill: false, | |
pointRadius: 4, | |
pointHoverRadius: 8, | |
} | |
} | |
getSpecialSettingsBy ( /** @type {String} */ elementId ) | |
{ | |
const parts = elementId.split( '-measurement-' ); | |
const measurementName = parts[ 1 ]; | |
const sensorName = parts[ 0 ].split( 'sensor-' )[ 1 ]; | |
const units = ( | |
typeof this.settings.units[ sensorName ] !== 'undefined' | |
&& typeof this.settings.units[ sensorName ][ measurementName ] !== 'undefined' | |
) ? this.settings.units[ sensorName ][ measurementName ] : false; | |
const systematicError = ( | |
typeof this.settings.graphs.special[ sensorName ] !== 'undefined' | |
&& typeof this.settings.graphs.special[ sensorName ][ measurementName ] !== 'undefined' | |
) ? this.settings.graphs.special[ sensorName ][ measurementName ] : false; | |
return { | |
measurement: measurementName, | |
sensor: sensorName, | |
units: units, | |
systematicError: systematicError, | |
}; | |
} | |
createGraphsFromInit ( /** @type {Object} */ data ) | |
{ | |
for ( const elementId in data ) { | |
const devices = data[ elementId ]; | |
const rootElement = document.getElementById( elementId ); | |
const timeScale = []; | |
let datasets = []; | |
for ( const name in devices ) { | |
const device = devices[ name ]; | |
const dataset = this.createNewDatasetWith( name ); | |
for ( const i in device ) { | |
const hitTime = new Date( device[ i ].date ); | |
timeScale.push( hitTime ); | |
dataset.data.push( { | |
x: hitTime, | |
y: device[ i ].value, | |
} ); | |
} | |
datasets.push( dataset ); | |
} | |
const specialSettings = this.getSpecialSettingsBy( elementId ); | |
if ( specialSettings.systematicError ) { | |
datasets = this.addDatasetsBy( specialSettings.measurement, datasets, specialSettings.systematicError ); | |
} | |
//datasets = this.addDatasetsBy( elementId.split( '-measurement-' )[ 1 ], datasets ); // @todo : make it asynchronous | |
/** @type {HTMLCanvasElement} */ | |
const canvasElement = ( document.createElement( 'canvas' ) ); | |
rootElement.appendChild( canvasElement ); | |
this.chartReferences[ elementId ] = new Chart( canvasElement.getContext( '2d' ), { // @type {t} | |
type: 'line', | |
data: { | |
datasets: datasets, | |
}, | |
options: { | |
responsive: true, | |
scales: { | |
xAxes: [ { | |
type: 'time', | |
display: true, | |
time: { | |
displayFormats: { | |
minute: 'h.mm' | |
}, | |
tooltipFormat: 'D. M. YYYY - HH.mm:ss' | |
}, | |
scaleLabel: { | |
display: true, | |
labelString: 'time scale', | |
} | |
} ], | |
yAxes: [ { | |
scaleLabel: { | |
display: true, | |
labelString: this.addUnitsBy( elementId, true ), | |
}, | |
} ] | |
}, | |
title: { | |
display: true, | |
fontSize: 16, // in px | |
text: elementId, | |
}, | |
legend: { | |
position: 'bottom', | |
}, | |
elements: { | |
point: { | |
//pointStyle: 'rectRot', | |
} | |
}, | |
}, | |
} ); | |
this.createColorsFrom( datasets ).then( ( colorsInOrder ) => | |
{ | |
for ( const i in colorsInOrder ) { | |
this.chartReferences[ elementId ].data.datasets[ i ].backgroundColor = colorsInOrder[ i ]; | |
this.chartReferences[ elementId ].data.datasets[ i ].borderColor = colorsInOrder[ i ]; | |
this.chartReferences[ elementId ].update(); | |
} | |
} ); | |
if ( elementId === 'sensor-co2-meter-measurement-concentration' ) { | |
const hex2rgba = ( hex, alpha = 1 ) => | |
{// @todo : to custom string prototype | |
const regexpString = ( hex.length > 4 ) ? '\\w\\w' : '\\w'; | |
const [ r, g, b ] = hex.match( new RegExp( regexpString, 'g' ) ).map( x => parseInt( x, 16 ) ); | |
return `rgba(${ r },${ g },${ b },${ alpha })`; | |
}; | |
const color = this.CHART_COLORS.lightness300; | |
this.chartReferences[ elementId ].options.tooltips = { | |
callbacks: { | |
label: ( item ) => | |
{ | |
return `co\u2082 concentration: ${ item.yLabel } ${ this.addUnitsBy( elementId ) }`; | |
}, | |
}, | |
}, | |
this.chartReferences[ elementId ].options.scales.yAxes[ 0 ].ticks.callback = function ( value ) | |
{ | |
if ( value === 10000 ) { | |
return 'Out of range!'; | |
} else if ( value === 2500 ) { | |
return 'Dangerous co2 > 2500' + value; | |
} else if ( value === 2000 ) { | |
return 'Poor air quality > ' + value; | |
} else if ( value === 1000 ) { //Acceptable level by ASHRAE and OSHA standards | |
return 'Acceptable level > ' + value; | |
} else if ( value === 500 ) { | |
return 'Good Indoor air > ' + value; | |
} else if ( value === 300 ) { | |
return 'Good Outdoor air > ' + value; | |
} else { | |
return value; | |
} | |
}; | |
this.chartReferences[ elementId ].options.annotation = { | |
annotations: [ { | |
type: 'box', | |
yScaleID: 'y-axis-0', | |
yMin: 2000, | |
borderColor: hex2rgba( color.red, 0.1 ), | |
backgroundColor: hex2rgba( color.red, 0.1 ), | |
}, { | |
type: 'box', | |
yScaleID: 'y-axis-0', | |
yMin: 1000, | |
yMax: 2000, | |
borderColor: hex2rgba( color.orange, 0.1 ), | |
backgroundColor: hex2rgba( color.orange, 0.1 ), | |
}, { | |
type: 'box', | |
yScaleID: 'y-axis-0', | |
yMin: 300, | |
yMax: 500, | |
borderColor: hex2rgba( color.green, 0.1 ), | |
backgroundColor: hex2rgba( color.green, 0.1 ), | |
}, { | |
type: 'box', | |
yScaleID: 'y-axis-0', | |
yMax: 300, | |
borderColor: hex2rgba( color.blue, 0.1 ), | |
backgroundColor: hex2rgba( color.blue, 0.1 ), | |
} ], | |
}; | |
} | |
} | |
} | |
connectWebSockets () | |
{ | |
const ws = new WebSocket( this.settings.wsSource.href ); | |
ws.addEventListener( 'open', function () | |
{ | |
this.settings.statistics.wsStatus.textContent = '\u2713'; //CHECK MARK | |
this.settings.statistics.wsStatus.title = 'connected'; | |
}.bind( this ) ); | |
ws.addEventListener( 'close', function () | |
{ | |
this.settings.statistics.wsStatus.textContent = '\u2715'; //Multiplication X | |
this.settings.statistics.wsStatus.title = 'closed'; | |
setTimeout( () => | |
{ | |
this.connectWebSockets(); | |
}, 5000 ); | |
}.bind( this ) ); | |
/* | |
ws.addEventListener( 'error', () => | |
{ | |
} ); | |
*/ | |
this.ws = ws; | |
} | |
createObjectFrom ( /** @type {String} */ hit ) | |
{ | |
/** @type {Array} */ | |
const stringParts = hit.split( '/' ); | |
/** @type {Array} */ | |
const deviceParts = stringParts[ 1 ].split( ':' ); | |
/** @type {Array} */ | |
const importantParts = stringParts[ 4 ].split( '♥' ); | |
/** @type {String | Number} */ | |
let value = importantParts[ 1 ]; | |
if ( value === 'true' ) { | |
value = 1; | |
} else if ( value === 'false' ) { | |
value = 0; | |
} else { | |
value = Number( value ); | |
} | |
return { | |
'device': deviceParts[ 0 ], | |
'deviceNo': Number( deviceParts[ 1 ] ), | |
'sensor': stringParts[ 2 ], | |
'measurement': importantParts[ 0 ], | |
'value': value, | |
'unit': '?' | |
}; | |
} | |
fetchWebSocketsMessages () | |
{ | |
this.ws.addEventListener( 'message', function ( /** @type {MessageEvent} */ event ) | |
{ | |
const measurementObject = this.createObjectFrom( event.data ); | |
measurementObject.timestamp = event.timeStamp; | |
this.storeHitToIndexedDB( measurementObject ); | |
}.bind( this ) ); | |
} | |
updateHitsCount ( /** @type {Number} */ how ) | |
{ | |
this.settings.statistics.nHitsElement.textContent = String( Number( this.settings.statistics.nHitsElement.textContent ) + how ); | |
return true; | |
} | |
updateGraphs ( /** @type {String} */ messageData ) | |
{ | |
const measurementObject = this.createObjectFrom( messageData ); | |
const graph = this.chartReferences[ this.getGraphIdFrom( measurementObject.sensor, measurementObject.measurement ) ]; | |
if ( !graph ) { | |
location.reload(); | |
return true; | |
} | |
const datasets = graph.data.datasets; | |
let datasetId = 0; | |
for ( const i in datasets ) { | |
if ( datasets[ i ].label === measurementObject.device + ':' + measurementObject.deviceNo ) { | |
datasetId = Number( i ); | |
break; | |
} | |
} | |
graph.data.datasets[ datasetId ].data.push( { | |
x: new Date(), | |
y: measurementObject.value, | |
} ); | |
if ( measurementObject.sensor === 'co2-meter' && measurementObject.measurement === 'concentration' ) { | |
console.log( '@todo : přišla nová data do CO2 meteru a tak bych měl updateovat i obalové zóny' ); | |
} | |
graph.update(); | |
} | |
liveViewUpdate () | |
{ | |
this.ws.addEventListener( 'message', function ( /** @type {MessageEvent} */ event ) | |
{ | |
this.updateHitsCount( +1 ); | |
if ( !Object.keys( this.chartReferences ).length ) { | |
location.reload(); | |
return true; | |
} | |
this.updateGraphs( event.data ); | |
}.bind( this ) ); | |
return true; | |
} | |
/* | |
* @todo : description | |
* @returns {Boolean} | |
*/ | |
run () | |
{ | |
this.prepareIndexedDB(); | |
this.connectWebSockets(); | |
this.fetchWebSocketsMessages(); | |
this.liveViewUpdate(); | |
this.countHits().then( ( /** @type {Number} */ result ) => | |
{ | |
this.settings.statistics.nHitsElement.textContent = String( result ); | |
} ); | |
this.getSensors().then( ( /** @type {Array} */ result ) => | |
{ | |
this.createGraphAreasBy( result ); | |
this.getAllGraphValues( result ).then( ( result ) => | |
{ | |
this.createGraphsFromInit( result ); | |
} ); | |
} ); | |
return true; | |
} | |
}; | |
/* | |
* Example usage: | |
* | |
<script src="iotic.js"></script> | |
<script> | |
const a = new iotic(window); | |
//a.settings = {'lang': 'cs', '…': true}; | |
a.run(); | |
//const cs = window.document.currentScript; | |
//cs.parentNode.removeChild(cs); | |
</script> | |
*/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[{"id":"2c41a2bd.aa36ae","type":"tab","label":"Flow 1"},{"id":"136f69ee.901e26","type":"websocket out","z":"2c41a2bd.aa36ae","name":"","server":"73e7d335.fa127c","client":"","x":810,"y":500,"wires":[]},{"id":"3198bdbd.ce6762","type":"http response","z":"2c41a2bd.aa36ae","name":"view","statusCode":"","headers":{},"x":570,"y":480,"wires":[]},{"id":"91fac4f9.e60978","type":"http in","z":"2c41a2bd.aa36ae","name":"","url":"/iot","method":"get","upload":false,"swaggerDoc":"","x":110,"y":480,"wires":[["efa7ba5f.b267b8"]]},{"id":"efa7ba5f.b267b8","type":"template","z":"2c41a2bd.aa36ae","name":"index template","field":"payload","fieldType":"msg","format":"html","syntax":"mustache","template":"<!DOCTYPE HTML>\n<html>\n <head>\n <title>Simple Live Display</title>\n <script type=\"text/javascript\">\n var ws;\n var wsUri = \"ws:\";\n var loc = window.location;\n if (loc.protocol === \"https:\") { wsUri = \"wss:\"; }\n // This needs to point to the web socket in the Node-RED flow\n // ... in this case it's ws/iot\n wsUri += \"//\" + loc.host + loc.pathname.replace(\"iot\",\"ws/iot\");\n\n function wsConnect() {\n console.log(\"connect\",wsUri);\n ws = new WebSocket(wsUri);\n //var line = \"\"; // either uncomment this for a building list of messages\n ws.onmessage = function(msg) {\n var line = \"\"; // or uncomment this to overwrite the existing message\n // parse the incoming message as a JSON object\n var data = msg.data;\n // build the output from the topic and payload parts of the object\n line += \"<p>\"+data+\"</p>\";\n // replace the messages div with the new \"line\"\n document.getElementById('messages').innerHTML = line;\n //ws.send(JSON.stringify({data:data}));\n }\n ws.onopen = function() {\n // update the status div with the connection status\n document.getElementById('status').innerHTML = \"connected\";\n //ws.send(\"Open for data\");\n console.log(\"connected\");\n }\n ws.onclose = function() {\n // update the status div with the connection status\n document.getElementById('status').innerHTML = \"not connected\";\n // in case of lost connection tries to reconnect every 3 secs\n setTimeout(wsConnect,3000);\n }\n }\n \n function doit(m) {\n if (ws) { ws.send(m); }\n }\n </script>\n </head>\n <body onload=\"wsConnect();\" onunload=\"ws.disconnect();\">\n <font face=\"Arial\">\n <h1>Simple Live Display</h1>\n <div id=\"messages\"></div>\n <button type=\"button\" onclick='doit(\"click\");'>Click to send message</button>\n <hr/>\n <div id=\"status\">unknown</div>\n </font>\n </body>\n</html>\n","x":380,"y":480,"wires":[["3198bdbd.ce6762"]]},{"id":"5e61ae55.9f5f7","type":"websocket in","z":"2c41a2bd.aa36ae","name":"","server":"73e7d335.fa127c","client":"","x":600,"y":560,"wires":[["3ef8a4bc.f82a8c"]]},{"id":"3ef8a4bc.f82a8c","type":"debug","z":"2c41a2bd.aa36ae","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","x":790,"y":560,"wires":[]},{"id":"c3ec03c3.12225","type":"inject","z":"2c41a2bd.aa36ae","name":"List all gateways","topic":"gateway/all/info/get","payload":"","payloadType":"str","repeat":"","crontab":"","once":false,"x":200,"y":40,"wires":[["e72a5880.83cf28"]]},{"id":"72c8911f.1547d","type":"inject","z":"2c41a2bd.aa36ae","name":"Start node pairing","topic":"gateway/usb-dongle/pairing-mode/start","payload":"","payloadType":"str","repeat":"","crontab":"","once":false,"x":200,"y":160,"wires":[["e72a5880.83cf28"]]},{"id":"a9abf84b.540bc8","type":"inject","z":"2c41a2bd.aa36ae","name":"Stop node pairing","topic":"gateway/usb-dongle/pairing-mode/stop","payload":"","payloadType":"str","repeat":"","crontab":"","once":false,"x":200,"y":220,"wires":[["e72a5880.83cf28"]]},{"id":"a2c24308.38cf9","type":"inject","z":"2c41a2bd.aa36ae","name":"List paired nodes","topic":"gateway/usb-dongle/nodes/get","payload":"","payloadType":"str","repeat":"","crontab":"","once":false,"x":200,"y":100,"wires":[["e72a5880.83cf28"]]},{"id":"c71f2fe0.04488","type":"inject","z":"2c41a2bd.aa36ae","name":"Unpair all nodes","topic":"gateway/usb-dongle/nodes/purge","payload":"","payloadType":"str","repeat":"","crontab":"","once":false,"x":200,"y":280,"wires":[["e72a5880.83cf28"]]},{"id":"e72a5880.83cf28","type":"mqtt out","z":"2c41a2bd.aa36ae","name":"","topic":"","qos":"","retain":"","broker":"f497470f.9bb198","x":810,"y":40,"wires":[]},{"id":"56ec7a60.e721c4","type":"mqtt in","z":"2c41a2bd.aa36ae","name":"","topic":"#","qos":"2","broker":"b06e568d.747148","x":90,"y":380,"wires":[["745aea92.342ab4"]]},{"id":"745aea92.342ab4","type":"function","z":"2c41a2bd.aa36ae","name":"format data","func":"msg.payload = msg.topic + \"♥\" + msg.payload;\nreturn msg;","outputs":1,"noerr":0,"x":370,"y":380,"wires":[["136f69ee.901e26"]]},{"id":"73e7d335.fa127c","type":"websocket-listener","z":"","path":"ws/iot","wholemsg":"false"},{"id":"f497470f.9bb198","type":"mqtt-broker","z":"","broker":"127.0.0.1","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","willTopic":"","willQos":"0","willPayload":""},{"id":"b06e568d.747148","type":"mqtt-broker","z":"","broker":"127.0.0.1","port":"1883","clientid":"","usetls":false,"compatmode":true,"keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","willTopic":"","willQos":"0","willPayload":""}] |
Author
iiic
commented
Feb 23, 2019
- https://github.com/bigclownlabs/bch-playground/releases
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment