tfmalt/power-meter-monitor

View on GitHub
lib/RaspberryMeter.js

Summary

Maintainability
A
25 mins
Test Coverage
/**
 * Created by tm on 11/04/15.
 *
 * @author Thomas Malt <thomas@malt.no>
 * @copyright 2013-2017 (c) Thomas Malt
 * @license MIT
 */

const Gpio         = require('onoff').Gpio;
const EventEmitter = require('events').EventEmitter;
const CronEmitter  = require('cron-emitter');

/**
 * Constructor for the raspberry pi based power meter.
 * Takes a reference to the redis database object as argument
 *
 * @param {object} redis Instance of redis connection
 * @constructor
 */
class RaspberryMeter extends EventEmitter {
  constructor(redis, options = {}) {
    super();
    this.constructor.sensor = new Gpio(18, 'in', 'both');
    this.constructor.led    = new Gpio(17, 'out');

    this.emitter            = new CronEmitter();
    this.options            = options;
    this.resetCounterFlag   = false;
    this.counter            = 0;
    this.pulses             = [];
    this.db                 = redis;
    this.start              = new Date();
    this.currentSensorValue = 0;

    this.limits = {
      seconds:       9000,
      minutes:       1560,
      fiveMinutes:   2304,
      thirtyMinutes: 1536,
      sixHours:      2200
    };

    this.handleSensorInterrupt = this.handleSensorInterrupt.bind(this);
    this.storeData             = this.storeData.bind(this);
    this.updateMeterTotal      = this.updateMeterTotal.bind(this);
    this.verifyLimit           = this.verifyLimit.bind(this);
    this.resetCounter          = this.resetCounter.bind(this);
  }

  get sensor() {
    return this.constructor.sensor;
  }

  get led() {
    return this.constructor.led;
  }

  /**
   * Starts the monitoring. Wathes the sensor
   * sets up event handler
   *
   * @emits 'started'
   * @returns {undefined}
   */
  startMonitor() {
    this.emit('started');

    this.sensor.watch(this.handleSensorInterrupt);

    this.emitter.add(`*/${this.options.interval} * * * * *`, 'time_to_store_data');
    this.emitter.on('time_to_store_data', () => this.storeData());

    return this;
  }

  /**
   * Handler for stuff to be done every second.
   *
   * @returns {Promise} resolved promise of what it does every second.
   */
  storeData() {
    const imps = this.options.impsPerKwh;
    const kwh  = counter => counter / imps;
    const watt = (k, seconds) => k * 3600 * imps / seconds;

    // adding one to counter to combat skew.
    // this.counter++;
    if (this.currentSensorValue === 0) this.counter++;

    const data = {
      timestamp:  Date.now(),
      time:       (new Date()).toJSON(),
      pulses:     this.counter,
      kwh:        kwh(this.counter),
      watt:       watt(kwh(this.counter), this.options.interval),
      interval:   this.options.interval
    };

    const currSensor = this.currentSensorValue;

    return this.storeSecondInDb(data)
      .then(this.updateMeterTotal)
      .then(total => this.emit('stored_data', {data, total, currSensor}))
      .then(() => this.verifyLimit())
      .then(() => (this.resetCounterFlag = true))
      .catch(error => console.log(
        'Error in storeData: ', error.message, error.stack, data
      ));
  }

  /**
   * @returns {undefined}
   * @private
   */
  resetCounter() {
    // this.emit('reset_counter', {counter: this.counter, pulses: this.pulses});
    this.counter = 0;
    this.pulses  = [];
    this.resetCounterFlag = false;
  }

  /**
   * Takes the data and stores it in the database
   *
   * @param {object} data A json object
   * @returns {Promise} the result of rpush
   * @private
   */
  storeSecondInDb(data) {
    return this.db.rpushAsync('seconds', JSON.stringify(data))
      .then(() => data);
  }

  /**
   * Increments the meter total with a new value every second.
   *
   * @param {object} data json
   * @returns {Promise} data
   */
  updateMeterTotal(data) {
    if (typeof data.kwh === 'undefined' || isNaN(data.kwh)) {
      throw new TypeError('A correct data json needs to be passed');
    }

    const db = this.db;
    return db.getAsync('meterTotal')
      .then((value = 0) => (parseFloat(value) + parseFloat(data.kwh)))
      .then(value => db.setAsync('meterTotal', value))
      .then(() => db.getAsync('meterTotal'));
  }

  /**
   * Checks length of seconds against limit and removes extra items
   *
   * @returns {boolean} true or false
   * @private
   */
  verifyLimit() {
    const db = this.db;
    return db.llenAsync('seconds')
      .then((length) => {
        if (length > this.limits.seconds) {
          db.ltrim('seconds', (length - this.limits.seconds), -1);
        }
      })
      .then(() => db.llenAsync('seconds'));
  }

  /**
   * @param {object} error error object
   * @param {integer} value integer value
   * @returns {boolean} true if state has changed, false if misfire.
   * @private
   */
  handleSensorInterrupt(error, value) {
    if (error) throw error;

    const pulseLength = this.getPulseLength();

    if (this.isSensorStateChanged(value) === false) {
      this.updateLastPulse(value, pulseLength);
      return false;
    }

    if (pulseLength < 5) return false;
    if (value === 1) this.counter++;
    if (value === 0 && this.resetCounterFlag === true) this.resetCounter();

    this.currentSensorValue = value;
    this.updateLed(value);
    this.addPulse(value, pulseLength);
    this.resetPulseStart();

    return true;
  }

  /**
   * @param {integer} value the value
   * @return {boolean} if sensor state has changed
   */
  isSensorStateChanged(value) {
    const last = this.getLastPulse();
    return (typeof last === 'undefined') ? true : value !== last.value;
  }

  /**
   * @param {integer} value 0 or 1 to write to the led.
   * @returns {undefined}
   * @private
   */
  updateLed(value) {
    this.led.writeSync(value);
  }

  /**
   * Calculates the interval since the last pulse event
   *
   * @returns {number} the length of pulse in milliseconds
   * @private
   */
  getPulseLength() {
    return Date.now() - this.start.getTime();
  }

  /**
   * @private
   * @returns {undefined}
   */
  resetPulseStart() {
    this.start = new Date();
  }

  /**
   * @returns {object} information about the last pulse
   */
  getLastPulse() {
    return this.pulses[this.pulses.length - 1];
  }

  updateLastPulse(value, length) {
    const last = this.getLastPulse();

    if (last.value !== value) {
      throw new Error('Value in input does not match value in array');
    }

    this.pulses[this.pulses.length - 1].length = last.length + length;
  }

  /**
   * @param {integer} value the value
   * @param {integer} length the length
   * @returns {undefined}
   * @private
   */
  addPulse(value, length) {
    this.pulses.push({value: value, length: length});
  }
}

module.exports = RaspberryMeter;

/*
 * MIT LICENSE
 *
 * Copyright (C) 2013-2017 Thomas Malt <thomas@malt.no>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to permit
 * persons to whom the Software is furnished to do so, subject to the
 * following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
 * OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */