Skip to content

Instantly share code, notes, and snippets.

@iiic
Last active February 23, 2019 15:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save iiic/f932a9861af5d688be3bcd897ad030f4 to your computer and use it in GitHub Desktop.
Save iiic/f932a9861af5d688be3bcd897ad030f4 to your computer and use it in GitHub Desktop.
iotic project
<!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>
'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>
*/
[{"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":""}]
@iiic
Copy link
Author

iiic commented Feb 23, 2019

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment