Source: plugins/powerspectrum/structure_plugin.js

/* structure_plugin.js (c) 2016 by Christian Mayer [CometVisu at ChristianMayer dot de]
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 3 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA
*/


/**
 * The powerspectrum plugin and widget creates a graph to show the power 
 * spectral data that the Enertex Smartmeter can send on the KNX bus.
 * 
 * @module plugins/powerspectrum/structure_plugin
 * @requires structure/pure
 * @author Christian Mayer
 * @since 0.10.0
 * */

require.config({
  shim: {
    'plugins/diagram/dep/flot/jquery.flot.min':          ['jquery'],
    'plugins/diagram/dep/flot/jquery.flot.canvas.min':   ['plugins/diagram/dep/flot/jquery.flot.min'],
    'plugins/diagram/dep/flot/jquery.flot.resize.min':   ['plugins/diagram/dep/flot/jquery.flot.min'],
    'plugins/diagram/dep/flot/jquery.flot.navigate.min': ['plugins/diagram/dep/flot/jquery.flot.min']
  }
});

define( ['structure_custom',
    'plugins/diagram/dep/flot/jquery.flot.min',
    'plugins/diagram/dep/flot/jquery.flot.canvas.min',
    'plugins/diagram/dep/flot/jquery.flot.resize.min',
    'plugins/diagram/dep/flot/jquery.flot.navigate.min'
  ], function( VisuDesign_Custom ) {
  "use strict";
  
  // Constants
  var
    CURRENT = 0,
    VOLTAGE = 1,
    limitEN50160_1999 = [[2,0.02],[3,0.05],[4,0.01],[5,0.06],[6,0.005],[7,0.05],
      [8,0.005],[9,0.015],[10,0.005],[11,0.035],[12,0.005],[13,0.03],[14,0.005],
      [15,0.005],[16,0.005],[17,0.02],[18,0.005],[19,0.015],[20,0.005],
      [21,0.005],[22,0.005],[23,0.015],[24,0.005],[25,0.015]
    ], // limit for voltage in ratio
    limitEN61000_3_2 = [[2,1.620],[3,3.450],[4,0.650],[5,1.710],[6,0.450],
      [7,1.160],[8,0.350],[9,0.600],[10,0.280],[11,0.500],[12,0.233],[13,0.320],
      [14,0.200],[15,0.230],[16,0.175],[17,0.203],[18,0.155],[19,0.182],
      [20,0.140],[21,0.164],[22,0.127],[23,0.150],[24,0.117],[25,0.139]
    ], // limit for current in Ampere
    referenceSin = [[],[],[]];
  
  // fix limits for better displaying
  function fixLimits( entry, index, array )
  {
    array[index][0] -= 0.5;
  }
  function lastShifted( array )
  {
    var last = array[ array.length-1 ];
    return [ last[0]+1, last[1] ];
  }
  
  limitEN50160_1999.forEach( fixLimits );
  limitEN50160_1999.push( lastShifted( limitEN50160_1999 ) );
  limitEN61000_3_2.forEach( fixLimits );
  limitEN61000_3_2.push( lastShifted( limitEN61000_3_2 ) );
  
  // fill reference
  for( var phi = 0; phi < 50; phi++ )
  {
    var
      time = phi * 20 / 50; // time in milliseconds
    
    referenceSin[0].push( [ time, Math.sin( (phi      ) * Math.PI / 25 ) ] );
    referenceSin[1].push( [ time, Math.sin( (phi+ 50/3) * Math.PI / 25 ) ] );
    referenceSin[2].push( [ time, Math.sin( (phi+100/3) * Math.PI / 25 ) ] );
  }
  
  /**
   * Setup helper
   */
  function setupCurve()
  {
    return [[0,0],[0.4,0],[0.8,0],[1.2,0],[1.6,0],[2,0],[2.4,0],[2.8,0],[3.2,0],
    [3.6,0],[4,0],[4.4,0],[4.8,0],[5.2,0],[5.6,0],[6,0],[6.4,0],[6.8,0],[7.2,0],
    [7.6,0],[8,0],[8.4,0],[8.8,0],[9.2,0],[9.6,0],[10,0],[10.4,0],[10.8,0],
    [11.2,0],[11.6,0],[12,0],[12.4,0],[12.8,0],[13.2,0],[13.6,0],[14,0],
    [14.4,0],[14.8,0],[15.2,0],[15.6,0],[16,0],[16.4,0],[16.8,0],[17.2,0],
    [17.6,0],[18,0],[18.4,0],[18.8,0],[19.2,0],[19.6,0]];
  }
  function setupSpectrum( offset )
  {
    var ret_val = [];
    
    if( undefined === offset )
      offset = 0;
    
    for( var i = 2; i < 52; i++ )
      ret_val.push( [ i + offset, 0 ] );
    return ret_val;
  }
  
  /**
   * Convert a spectrum to a curve
   */
  function updateCurve( input, target, phase )
  {
    var 
      inp = input[phase],
      out = target[phase],
      shift = (phase * 2 / 3 - 0.5) * Math.PI;
      
    for( var i = 0; i < 50; i++ )
    {
      var
        phi  = i * Math.PI / 25,
        value = Math.cos( phi + shift ); // the base with 50 Hz
        
      // the harmonics
      for( var j = 2; j < 50; j++ )
      {
        value += Math.cos( (phi+shift) * j ) * inp[j][1];
      }
      
      out[i][1] = value;
    }
  }
  
  /**
   * Little helper to setup the Flot dataset structure.
   */
  function createDatasetCurve( widgetData )
  {
    return widgetData.singlePhase ?
      [
        { label: null, data: referenceSin[0], color:13 }, // trick flot to automatically make color darker
        { label: 'L', data: widgetData.curve[0], color:1 }
      ] :
      [
        { label: null, data: referenceSin[0], color:13 },
        { label: null, data: referenceSin[1], color:14 },
        { label: null, data: referenceSin[2], color:15 },
        { label: 'L1', data: widgetData.curve[0], color:1 },
        { label: 'L2', data: widgetData.curve[1], color:2 },
        { label: 'L3', data: widgetData.curve[2], color:3 }
      ];
  }
  
  /**
   * Little helper to setup the Flot dataset structure.
   */
  function createDatasetSpectrum( widgetData )
  {
    return widgetData.singlePhase ? 
      [
        { label: widgetData.limitName, data:widgetData.displayType===VOLTAGE?limitEN50160_1999:limitEN61000_3_2, bars:{show:false}, lines:{steps:true}, color:0 },
        { label: widgetData.name1, data:widgetData.spectrum[0] , color:1}
      ] :
      [
        { label: widgetData.limitName, data:widgetData.displayType===VOLTAGE?limitEN50160_1999:limitEN61000_3_2, bars:{show:false}, lines:{steps:true}, color:0 },
        { label: widgetData.name1, data:widgetData.spectrum[0], color:1 },
        { label: widgetData.name2, data:widgetData.spectrum[1], color:2 },
        { label: widgetData.name3, data:widgetData.spectrum[2], color:3 }
      ];
  }

  /**
   * This will create a diagram to show the frequency that the Enertex 
   * Smartmeter pushes on the KNX bus.
   */
  VisuDesign_Custom.prototype.addCreator( 'powerspectrum', {
    create: function( element, path, flavour, type ) {
      var 
        $e = $(element),
        displayType = $e.attr('type') === 'current' ? CURRENT : VOLTAGE,
        singlePhase = $e.attr('singlephase') === 'true' ? true : false;
      
      // create the main structure
      function handleVariant(src, transform, mode, variant)
      {
        if( !variant )
          variant = 'spectrum'; // the default
        
        return [true, variant];
      }
    
      var ret_val = templateEngine.design.createDefaultWidget( 'powerspectrum', $e, path, flavour, type, this.update, handleVariant );
      var data = templateEngine.widgetDataInsert( path, {
        displayType: displayType,
        singlePhase: singlePhase,
        spectrum: singlePhase ? [ setupSpectrum() ] : [ setupSpectrum(-0.26), setupSpectrum(0), setupSpectrum(0.26) ],
        limitName: $e.attr('limitname') || 'limit',
        name1: $e.attr('name1') || (singlePhase ? 'L' : 'L1'),
        name2: $e.attr('name2') || 'L2',
        name3: $e.attr('name3') || 'L3',
        curve: singlePhase ? [ setupCurve() ] : [ setupCurve(), setupCurve(), setupCurve() ],
        current: [],
        showCurve: $e.attr('spectrumonly') === 'true' ? false : true,
        showLegend: $e.attr('showlegend') === 'true' ? true : false,
        colors: [
          $e.attr('limitcolor') || "#edc240", // default directly from flot code
          $e.attr('color1')     || "#afd8f8",
          $e.attr('color2')     || "#cb4b4b",
          $e.attr('color3')     || "#4da74d"
        ]
      });

      // create the actor
      var actor = '<div class="actor clickable">';
      if( data.showCurve )
        actor += '<div class="diagram_inline curve">loading...</div>';
      actor += '<div class="diagram_inline spectrum">loading...</div></div>';

      this.construct(path);
      
      return ret_val + actor + '</div>';
    },

    construct: function(path) {
      var data = templateEngine.widgetDataGet(path);
      templateEngine.messageBroker.subscribe("setup.dom.finished", function() {
        var
          diagramCurve = data.showCurve && $( '#' + path + ' .actor div.curve' ).empty(),
          optionsCurve = data.showCurve && {
              colors: data.colors,
              legend: {
                show: data.showLegend // default to false
              },
              xaxis: {
                show: false
              },
              yaxis: {
                show: false
              }
            },
          diagramSpectrum = $( '#' + path + ' .actor div.spectrum' ).empty(),
          optionsSpectrum = {
            colors: data.colors,
            series: {
              bars: {
                show: true,
                fill: 1,
                fillColor: null,
                lineWidth: 0
              }
            },
            bars: {
              align: "center",
              barWidth: data.singlePhase ? 0.75 : 0.25
            },
            legend: {
              show: data.showLegend // default to true
            },
            xaxis: {
              show: false
            },
            yaxis: {
              show: false
            }
          };
        data.plotCurve = data.showCurve && $.plot(diagramCurve, createDatasetCurve( data ), optionsCurve);
        data.plot = $.plot(diagramSpectrum, createDatasetSpectrum( data ), optionsSpectrum);
      });
    },

    update: function( ga, data ) {
      var 
        element = $(this),
        widgetData = templateEngine.widgetDataGetByElement( this ),
        addressInfo = widgetData.address[ ga ];
        
      if( addressInfo[2][0] === 'I' )
      {
        var
          phase = widgetData.singlePhase ? 1 : +(addressInfo[2][1] || 1),
          value = templateEngine.transformDecode( addressInfo[0], data );
        widgetData.current[phase-1] = value / 1000; // transform mA to A
      } else if( 
        widgetData.address[ ga ][2].substr(0,8) === 'spectrum'
        && data.length === 28 ) // sanity check for 14 bytes
      {
        var 
          phase = widgetData.singlePhase ? 1 : +(addressInfo[2][8] || 1),
          index = parseInt( data.substr( 0, 2 ), 16 ),
          factor = widgetData.current[phase-1] || 1,
          values = [];

        for( var i = 0; i < 13; i++ )
        {
          if( index + i < 2 )
            continue;
          
          values[i] = Math.pow( 10, (parseInt( data.substr( i*2 + 2, 2 ), 16 ) - 253) / 80);
          widgetData.spectrum[phase-1][ index + i - 2 ][1] = values[i] * factor;
        }
        widgetData.plot.setData( createDatasetSpectrum( widgetData ) );
        widgetData.plot.draw();
        
        if( widgetData.plotCurve )
        {
          updateCurve( widgetData.spectrum, widgetData.curve, phase-1 );
          widgetData.plotCurve.setData( createDatasetCurve( widgetData ) );
          widgetData.plotCurve.draw();
        }
      }
    }
  });
});