<commit_before>(function (enyo, scope) {
	/**
	 * Fires when progress bar finishes animating to a position.
	 *
	 * @event mochi.ProgressBar#onAnimateProgressFinish
	 * @type {Object}
	 * @property {Object} sender - The [component]{@link enyo.Component} that most recently
	 *	propagated the [event]{@link external:event}.
	 * @property {enyo.Scroller~ScrollEvent} event - An [object]{@link external:Object} containing
	 *	[event]{@link external:event} information.
	 * @public
	 */

	/**
	 * _mochi.ProgressBar_ is a  control that shows the current progress of a
	 * process in a horizontal bar.
	 *
	 * ```
	 * {kind: 'mochi.ProgressBar', progress: 10}
	 * ```
	 *
	 * To animate progress changes, call the {@link mochi.ProgressBar#animateProgressTo} method:
	 *
	 * ```
	 * this.$.progressBar.animateProgressTo(50);
	 * ```
	 *
	 * You may customize the color of the bar by applying a style via the
	 * {@link mochi.ProgressBar#barClasses} property, e.g.:
	 *
	 * ```
	 * {kind: 'mochi.ProgressBar', barClasses: 'class-name'}
	 * ```
	 *
	 * @ui
	 * @class mochi.ProgressBar
	 * @extends enyo.Control
	 * @public
	 */
	enyo.kind(
		/** @lends mochi.ProgressBar.prototype */ {

		/**
		 * @private
		 */
		name: "mochi.ProgressBar",

		/**
		 * @private
		 */
		kind: 'enyo.Control',

		/**
		 * @private
		 */
		classes: "mochi-progress-bar",

		/**
		 * @private
		 */
		published: /** @lends mochi.ProgressBar.prototype */ {
			/**
			 * Current position of progress bar
			 *
			 * @type {Number}
			 * @default 0
			 * @public
			 */
			progress: 0,

			/**
			 * Minimum progress value (i.e., no progress made)
			 *
			 * @type {Number}
			 * @default 0
			 * @public
			 */
			min: 0,

			/**
			 * Maximum progress value (i.e., process complete)
			 *
			 * @type {Number}
			 * @default 100
			 * @public
			 */
			max: 100,

			/**
			 * CSS classes to apply to progress bar
			 *
			 * @type {String}
			 * @default 'mochi-progress-bar-bar'
			 * @public
			 */
			barClasses: "",
			/**
			 * Increment value (value by which slider increments upon move)
			 *
			 * @type {Number}
			 * @default 0
			 * @public
			 */
			increment: 0

		},

		/**
		 * @private
		 */
		events: {
			/**
			 * {@link mochi.ProgressBar#event:onAnimateProgressFinish}
			 */
			onAnimateProgressFinish: ""
		},

		/**
		 * @private
		 */
		components: [
			{name: "progressAnimator", kind: "Animator", onStep: "progressAnimatorStep", onEnd: "progressAnimatorComplete"},
			{name: "bar", classes: "mochi-progress-bar-bar"}
		],

		/**
		 * @private
		 */
		create: function() {
			this.inherited(arguments);
			this.progressChanged();
			this.barClassesChanged();
		},

		/**
		 * @private
		 */
		barClassesChanged: function(inOld) {
			this.$.bar.removeClass(inOld);
			this.$.bar.addClass(this.barClasses);
		},

		/**
		 * @private
		 */
		progressChanged: function() {
			this.progress = this.clampValue(this.min, this.max, this.progress);
			var p = this.calcPercent(this.progress);
			this.updateBarPosition(p);
		},

		/**
		 * @private
		 */
		clampValue: function(inMin, inMax, inValue) {
			return Math.max(inMin, Math.min(inValue, inMax));
		},

		/**
		 * @private
		 */
		calcRatio: function(inValue) {
			return (inValue - this.min) / (this.max - this.min);
		},

		/**
		 * @private
		 */
		calcPercent: function(inValue) {
			return this.calcRatio(inValue) * 100;
		},

		/**
		 * @private
		 */
		updateBarPosition: function(inPercent) {
			this.$.bar.applyStyle("width", inPercent + "%");
		},

		/**
		 * Animates progress to the passed-in value.
		 *
		 * @param {Number} inValue  The destination number
		 * @public
		 */
		animateProgressTo: function(inValue) {
			this.$.progressAnimator.play({
				startValue: this.progress,
				endValue: inValue,
				node: this.hasNode()
			});
		},

		/**
		 * @private
		 */
		progressAnimatorStep: function(inSender) {
			this.setProgress(inSender.value);
			return true;
		},

		/**
		 * @private
		 */
		progressAnimatorComplete: function(inSender) {
			this.doAnimateProgressFinish(inSender);
			return true;
		}
	});

})(enyo, this);<commit_msg>Added calcIncrement so Slider works correctly with increments<commit_after>(function (enyo, scope) {
	/**
	 * Fires when progress bar finishes animating to a position.
	 *
	 * @event mochi.ProgressBar#onAnimateProgressFinish
	 * @type {Object}
	 * @property {Object} sender - The [component]{@link enyo.Component} that most recently
	 *	propagated the [event]{@link external:event}.
	 * @property {enyo.Scroller~ScrollEvent} event - An [object]{@link external:Object} containing
	 *	[event]{@link external:event} information.
	 * @public
	 */

	/**
	 * _mochi.ProgressBar_ is a  control that shows the current progress of a
	 * process in a horizontal bar.
	 *
	 * ```
	 * {kind: 'mochi.ProgressBar', progress: 10}
	 * ```
	 *
	 * To animate progress changes, call the {@link mochi.ProgressBar#animateProgressTo} method:
	 *
	 * ```
	 * this.$.progressBar.animateProgressTo(50);
	 * ```
	 *
	 * You may customize the color of the bar by applying a style via the
	 * {@link mochi.ProgressBar#barClasses} property, e.g.:
	 *
	 * ```
	 * {kind: 'mochi.ProgressBar', barClasses: 'class-name'}
	 * ```
	 *
	 * @ui
	 * @class mochi.ProgressBar
	 * @extends enyo.Control
	 * @public
	 */
	enyo.kind(
		/** @lends mochi.ProgressBar.prototype */ {

		/**
		 * @private
		 */
		name: "mochi.ProgressBar",

		/**
		 * @private
		 */
		kind: 'enyo.Control',

		/**
		 * @private
		 */
		classes: "mochi-progress-bar",

		/**
		 * @private
		 */
		published: /** @lends mochi.ProgressBar.prototype */ {
			/**
			 * Current position of progress bar
			 *
			 * @type {Number}
			 * @default 0
			 * @public
			 */
			progress: 0,

			/**
			 * Minimum progress value (i.e., no progress made)
			 *
			 * @type {Number}
			 * @default 0
			 * @public
			 */
			min: 0,

			/**
			 * Maximum progress value (i.e., process complete)
			 *
			 * @type {Number}
			 * @default 100
			 * @public
			 */
			max: 100,

			/**
			 * CSS classes to apply to progress bar
			 *
			 * @type {String}
			 * @default 'mochi-progress-bar-bar'
			 * @public
			 */
			barClasses: "",
			/**
			 * Increment value (value by which slider increments upon move)
			 *
			 * @type {Number}
			 * @default 0
			 * @public
			 */
			increment: 0

		},

		/**
		 * @private
		 */
		events: {
			/**
			 * {@link mochi.ProgressBar#event:onAnimateProgressFinish}
			 */
			onAnimateProgressFinish: ""
		},

		/**
		 * @private
		 */
		components: [
			{name: "progressAnimator", kind: "Animator", onStep: "progressAnimatorStep", onEnd: "progressAnimatorComplete"},
			{name: "bar", classes: "mochi-progress-bar-bar"}
		],

		/**
		 * @private
		 */
		create: function() {
			this.inherited(arguments);
			this.progressChanged();
			this.barClassesChanged();
		},

		/**
		 * @private
		 */
		barClassesChanged: function(inOld) {
			this.$.bar.removeClass(inOld);
			this.$.bar.addClass(this.barClasses);
		},

		/**
		 * @private
		 */
		progressChanged: function() {
			this.progress = this.clampValue(this.min, this.max, this.progress);
			var p = this.calcPercent(this.progress);
			this.updateBarPosition(p);
		},

		/**
		 * @private
		 */
		clampValue: function(inMin, inMax, inValue) {
			return Math.max(inMin, Math.min(inValue, inMax));
		},

		/**
		 * @private
		 */
		calcRatio: function(inValue) {
			return (inValue - this.min) / (this.max - this.min);
		},

		/**
		 * @private
		 */
		calcPercent: function(inValue) {
			return this.calcRatio(inValue) * 100;
		},

		/**
		 * @private
		 */
		calcIncrement: function (value) {
			return (Math.round(value / this.increment) * this.increment);
		},

		/**
		 * @private
		 */
		updateBarPosition: function(inPercent) {
			this.$.bar.applyStyle("width", inPercent + "%");
		},

		/**
		 * Animates progress to the passed-in value.
		 *
		 * @param {Number} inValue  The destination number
		 * @public
		 */
		animateProgressTo: function(inValue) {
			this.$.progressAnimator.play({
				startValue: this.progress,
				endValue: inValue,
				node: this.hasNode()
			});
		},

		/**
		 * @private
		 */
		progressAnimatorStep: function(inSender) {
			this.setProgress(inSender.value);
			return true;
		},

		/**
		 * @private
		 */
		progressAnimatorComplete: function(inSender) {
			this.doAnimateProgressFinish(inSender);
			return true;
		}
	});

})(enyo, this);<|endoftext|>


<commit_before>// TODO:
// - Check success of Roon API calls
// - Volume control
// - Seek

// Parts of this code are copied from https://github.com/RoonLabs/roon-extension-powermate

"use strict";

var pjson = require('./package.json');
var package_version = pjson.version; //process.env.npm_package_version does not work under forever

// Roon setup

var RoonApi = require("node-roon-api"),
  RoonApiSettings = require('node-roon-api-settings'),
  RoonApiStatus = require('node-roon-api-status'),
  RoonApiTransport = require('node-roon-api-transport');

var core;
var zones = [];

var roon = new RoonApi({
  extension_id: 'roonrest',
  display_name: 'Roon Rest Controller',
  display_version: package_version,
  publisher: 'Matthew Eckhaus',
  email: 'contact@roonlabs.com',
  website: 'https://github.com/matteck/roonrest',

  core_paired: function (core_) {
    core = core_;

    core.services.RoonApiTransport.subscribe_zones((response, msg) => {
      if (response == "Subscribed") {
        let curZones = msg.zones.reduce((p, e) => (p[e.zone_id] = e) && p, {});
        zones = curZones;
      } else if (response == "Changed") {
        var z;
        if (msg.zones_removed) msg.zones_removed.forEach(e => delete (zones[e.zone_id]));
        if (msg.zones_added) msg.zones_added.forEach(e => zones[e.zone_id] = e);
        if (msg.zones_changed) msg.zones_changed.forEach(e => zones[e.zone_id] = e);
      }
    });

  },
  core_unpaired: function (core_) {
    core = undefined;
  }
});

var mysettings = roon.load_config("settings") || {
  zone: null,
};

function makelayout(settings) {
  var l = {
    values: settings,
    layout: [],
    has_error: false
  };

  l.layout.push({
    type: "zone",
    title: "Zone",
    setting: "zone",
  });

  return l;
}

var svc_settings = new RoonApiSettings(roon, {
  get_settings: function (cb) {
    cb(makelayout(mysettings));
  },
  save_settings: function (req, isdryrun, settings) {
    let l = makelayout(settings.values);
    req.send_complete(l.has_error ? "NotValid" : "Success", { settings: l });

    if (!isdryrun && !l.has_error) {
      mysettings = l.values;
      svc_settings.update_settings(l);
      roon.save_config("settings", mysettings);
      update_status();
    }
  }
});

var svc_status = new RoonApiStatus(roon);

roon.init_services({
  required_services: [RoonApiTransport],
  provided_services: [svc_settings, svc_status],
});

function update_status() {
  if (mysettings.hasOwnProperty("zone") && mysettings.zone != null && mysettings.zone.hasOwnProperty("name")) {
    svc_status.set_status("Ready. Attached to " + mysettings.zone.name, false);
  } else {
    svc_status.set_status("Loaded. No zone assigned");
  }
}

update_status();
roon.start_discovery();

// Express
var port = 3000

var express = require('express')
var app = express()

var not_registered_error = "The RoonRest extension is not enabled. Please enable it in Roon settings and try again.";

// Zone-specific control actions
app.put('/api/v1/zone/current/control/:action(play|pause|playpause|stop|previous|next)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var action = req.params['action'];
    console.log('action is ' + action);
    core.services.RoonApiTransport.control(mysettings.zone, action);
    res.send('OK');
  }
})

// Universal control actions
app.put('/api/v1/zone/all/control/pause', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    console.log('Doing pause_all');
    core.services.RoonApiTransport.pause_all();
    res.send('OK');
  }
})

// Settings
app.put('/api/v1/zone/current/settings/:name(shuffle|auto_radio)/:value(on|off)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    console.log('Doing set ' + req.params['name'] + ' to ' + req.params['value']);
    var settings_object = {};
    var setting_name = req.params['name']
    if (req.params['value'] == 'on') {
      settings_object[setting_name] = 1;
    } else {
      settings_object[setting_name] = 0;
    }
    core.services.RoonApiTransport.change_settings(mysettings.zone, settings_object);
    res.send('OK');
  }
})

// Volume
app.put('/api/v1/zone/current/volume/:how(absolute|relative|relative_step)/:value', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var how = req.params['how'];
    var value = req.params['value']
    console.log('volume how is ' + how);
    core.services.RoonApiTransport.change_volume(mysettings.zone, how, value);
    res.send('OK');
  }
})

app.get('/api/v1', function (req, res) {
  res.send('RoonRest extensions v1')
})

app.get('/api/v1/zones', function (req, res) {
  res.send(zones);
})

app.listen(port, function () {
  console.log('RoonRest extension started, listening on port ' + port)
})<commit_msg>Support Zone ID as parameter<commit_after>// TODO:
// - Check success of Roon API calls
// - Volume control
// - Seek

// Parts of this code are copied from https://github.com/RoonLabs/roon-extension-powermate

"use strict";

var pjson = require('./package.json');
var package_version = pjson.version; //process.env.npm_package_version does not work under forever

// Roon setup

var RoonApi = require("node-roon-api"),
  RoonApiSettings = require('node-roon-api-settings'),
  RoonApiStatus = require('node-roon-api-status'),
  RoonApiTransport = require('node-roon-api-transport');

var core;
var zones = [];

var roon = new RoonApi({
  extension_id: 'roonrest',
  display_name: 'Roon Rest Controller',
  display_version: package_version,
  publisher: 'Matthew Eckhaus',
  email: 'contact@roonlabs.com',
  website: 'https://github.com/matteck/roonrest',

  core_paired: function (core_) {
    core = core_;

    core.services.RoonApiTransport.subscribe_zones((response, msg) => {
      if (response == "Subscribed") {
        let curZones = msg.zones.reduce((p, e) => (p[e.zone_id] = e) && p, {});
        zones = curZones;
      } else if (response == "Changed") {
        var z;
        if (msg.zones_removed) msg.zones_removed.forEach(e => delete (zones[e.zone_id]));
        if (msg.zones_added) msg.zones_added.forEach(e => zones[e.zone_id] = e);
        if (msg.zones_changed) msg.zones_changed.forEach(e => zones[e.zone_id] = e);
      }
    });

  },
  core_unpaired: function (core_) {
    core = undefined;
  }
});

var mysettings = roon.load_config("settings") || {
  zone: null,
};

function makelayout(settings) {
  var l = {
    values: settings,
    layout: [],
    has_error: false
  };

  l.layout.push({
    type: "zone",
    title: "Zone",
    setting: "zone",
  });

  return l;
}

var svc_settings = new RoonApiSettings(roon, {
  get_settings: function (cb) {
    cb(makelayout(mysettings));
  },
  save_settings: function (req, isdryrun, settings) {
    let l = makelayout(settings.values);
    req.send_complete(l.has_error ? "NotValid" : "Success", { settings: l });

    if (!isdryrun && !l.has_error) {
      mysettings = l.values;
      svc_settings.update_settings(l);
      roon.save_config("settings", mysettings);
      update_status();
    }
  }
});

var svc_status = new RoonApiStatus(roon);

roon.init_services({
  required_services: [RoonApiTransport],
  provided_services: [svc_settings, svc_status],
});

function update_status() {
  if (mysettings.hasOwnProperty("zone") && mysettings.zone != null && mysettings.zone.hasOwnProperty("name")) {
    svc_status.set_status("Ready. Attached to " + mysettings.zone.name, false);
  } else {
    svc_status.set_status("Loaded. No zone assigned");
  }
}

update_status();
roon.start_discovery();

// Express
var port = 3000

var express = require('express')
var app = express()

var not_registered_error = "The RoonRest extension is not enabled. Please enable it in Roon settings and try again.";

// Zone-specific control actions
app.put('/api/v1/zone/:zone/control/:action(play|pause|playpause|stop|previous|next)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var action = req.params['action'];
    let this_zone = null;
    if (req.params['zone'] == 'current')
      this_zone = mysettings.zone;
    else
      this_zone = zones[req.params['zone']];
    core.services.RoonApiTransport.control(this_zone, action);
    res.send('OK');
  }
})

// Universal control actions
app.put('/api/v1/zone/all/control/pause', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    console.log('Doing pause_all');
    core.services.RoonApiTransport.pause_all();
    res.send('OK');
  }
})

// Settings
app.put('/api/v1/zone/:zone/settings/:name(shuffle|auto_radio)/:value(on|off)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    console.log('Doing set ' + req.params['name'] + ' to ' + req.params['value']);
    var settings_object = {};
    var setting_name = req.params['name']
    if (req.params['value'] == 'on') {
      settings_object[setting_name] = 1;
    } else {
      settings_object[setting_name] = 0;
    }
    let this_zone = null;
    if (req.params['zone'] == 'current')
      this_zone = mysettings.zone;
    else
      this_zone = zones[req.params['zone']];
    core.services.RoonApiTransport.change_settings(this_zone, settings_object);
    res.send('OK');
  }
})

// Volume
app.put('/api/v1/zone/:zone/volume/:how(absolute|relative|relative_step)/:value', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var how = req.params['how'];
    var value = req.params['value']
    console.log('volume how is ' + how);
    core.services.RoonApiTransport.change_volume(mysettings.zone, how, value);
    res.send('OK');
  }
})

app.get('/api/v1', function (req, res) {
  res.send('RoonRest extensions v1')
})

app.get('/api/v1/zones', function (req, res) {
  res.send(zones);
})

app.listen(port, function () {
  console.log('RoonRest extension started, listening on port ' + port)
})<|endoftext|>


<commit_before>// TODO:
// - Check success of Roon API calls
// - Volume control
// - Seek

// Parts of this code are copied from https://github.com/RoonLabs/roon-extension-powermate

"use strict";

var pjson = require('./package.json');
var package_version = pjson.version; //process.env.npm_package_version does not work under forever

var local_zone = '16015eda3fbd0e21524b9e67274de700f4ff' // zone id which runs on the local server, for screen control

// Roon setup

var RoonApi = require("node-roon-api"),
  RoonApiSettings = require('node-roon-api-settings'),
  RoonApiStatus = require('node-roon-api-status'),
  RoonApiTransport = require('node-roon-api-transport');

var core;
var zones = [];

var roon = new RoonApi({
  extension_id: 'roonrest',
  display_name: 'Roon Rest Controller',
  display_version: package_version,
  publisher: 'Matthew Eckhaus',
  email: 'contact@roonlabs.com',
  website: 'https://github.com/matteck/roonrest',

  core_paired: function (core_) {
    core = core_;

    core.services.RoonApiTransport.subscribe_zones((response, msg) => {
      if (response == "Subscribed") {
        let curZones = msg.zones.reduce((p, e) => (p[e.zone_id] = e) && p, {});
        zones = curZones;
      } else if (response == "Changed") {
        var z;
        if (msg.zones_removed) msg.zones_removed.forEach(e => delete (zones[e.zone_id]));
        if (msg.zones_added) msg.zones_added.forEach(e => zones[e.zone_id] = e);
        if (msg.zones_changed) msg.zones_changed.forEach(e => zones[e.zone_id] = e);
      }
    });

  },
  core_unpaired: function (core_) {
    core = undefined;
  }
});

var mysettings = roon.load_config("settings") || {
  zone: null,
};

function makelayout(settings) {
  var l = {
    values: settings,
    layout: [],
    has_error: false
  };

  l.layout.push({
    type: "zone",
    title: "Zone",
    setting: "zone",
  });

  return l;
}

var svc_settings = new RoonApiSettings(roon, {
  get_settings: function (cb) {
    cb(makelayout(mysettings));
  },
  save_settings: function (req, isdryrun, settings) {
    let l = makelayout(settings.values);
    req.send_complete(l.has_error ? "NotValid" : "Success", { settings: l });

    if (!isdryrun && !l.has_error) {
      mysettings = l.values;
      svc_settings.update_settings(l);
      roon.save_config("settings", mysettings);
      update_status();
    }
  }
});

var svc_status = new RoonApiStatus(roon);

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function screenblank() {
  // Update dpms settings whenever play state changes on local machine
  // It's easier to check the state directly than trying to guess based on the action
  if (!local_zone) {
    return;
  }
  // Hack - Zone state doesn't update immediately
  await sleep(250);
  var roon_state = zones[local_zone]['state'];
  var xset = "/usr/bin/xset -display :0.0";
  var timeout = "600";
  const { exec } = require('child_process');
  var cmd = xset + ' q';
  console.log('Doing ' + cmd)
  exec(cmd, (err, xsetout, stderr) => {
    if (err) {
      console.log("xset q failed");
      console.log(stderr);
      return;
    } else {
      var found = xsetout.match(/DPMS is (\w+)/);
      var dpms_state = found[1];
      if (roon_state == "paused") {
        if (dpms_state == "Disabled") {
          var cmd = xset + ' +dpms';
          console.log('Doing ' + cmd)
          exec(cmd, (err, stdout, stderr) => {
            if (err) {
              console.log("xset +dpms failed");
              console.log(stderr);
              return;
            }
          });
        }
        // DPMS is on, make sure it has the right timeout
        var found = xsetout.match(/Standby:\s+\d+\s+Suspend:\s+\d+\s+Off:\s+(\d+)/);
        if (found[1] != timeout) {
          var cmd = xset + ' dpms ' + timeout + ' ' + timeout + ' ' + timeout;
          console.log('Doing ' + cmd);
          exec(cmd, (err, stdout, stderr) => {
            if (err) {
              console.log("xset dpms timeout failed");
              console.log(stderr);
              return;
            }
          });
        }
      } else if (roon_state == "playing" && dpms_state == "Enabled") {
        var cmd = xset + ' -dpms';
        console.log('Doing ' + cmd)
        exec(cmd, (err, stdout, stderr) => {
          if (err) {
            console.log("xset -dpms failed");
            console.log(stderr);
            return;
          }
        });
      }
    }
  });
}

roon.init_services({
  required_services: [RoonApiTransport],
  provided_services: [svc_settings, svc_status],
});

function update_status() {
  if (mysettings.hasOwnProperty("zone") && mysettings.zone != null && mysettings.zone.hasOwnProperty("name")) {
    svc_status.set_status("Ready. Attached to " + mysettings.zone.name, false);
  } else {
    svc_status.set_status("Loaded. No zone assigned");
  }
}

update_status();
roon.start_discovery();

// Express
var port = 3000

var express = require('express')
var app = express()

var not_registered_error = "The RoonRest extension is not enabled. Please enable it in Roon settings and try again.";

// Universal control actions
app.put('/api/v1/zone/all/control/:action(pause)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    console.log('Doing pause_all');
    core.services.RoonApiTransport.pause_all(screenblank);
    res.send('OK');
  }
})

// Zone-specific control actions
app.put('/api/v1/zone/:zone/control/:action(play|pause|playpause|stop|previous|next)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var action = req.params['action'];
    console.log('Doing action ' + action)
    let this_zone = null;
    if (req.params['zone'] == 'current')
      this_zone = mysettings.zone;
    else
      this_zone = zones[req.params['zone']];
    if (this_zone == null) {
      res.status('404').send();
    }
    core.services.RoonApiTransport.control(this_zone, action, screenblank);
    res.send('OK');
  }
})

// Settings
app.put('/api/v1/zone/:zone/settings/:name(shuffle|auto_radio)/:value(on|off)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    console.log('Doing set ' + req.params['name'] + ' to ' + req.params['value']);
    var settings_object = {};
    var setting_name = req.params['name']
    if (req.params['value'] == 'on') {
      settings_object[setting_name] = 1;
    } else {
      settings_object[setting_name] = 0;
    }
    let this_zone = null;
    if (req.params['zone'] == 'current')
      this_zone = mysettings.zone;
    else
      this_zone = zones[req.params['zone']];
    core.services.RoonApiTransport.change_settings(this_zone, settings_object);
    res.send('OK');
  }
})

// Volume
app.put('/api/v1/zone/:zone/volume/:how(absolute|relative|relative_step)/:value', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var how = req.params['how'];
    var value = req.params['value']
    console.log('volume how is ' + how);
    core.services.RoonApiTransport.change_volume(mysettings.zone, how, value);
    res.send('OK');
  }
})

app.get('/api/v1', function (req, res) {
  res.send('RoonRest extensions v1')
})

app.get('/api/v1/zones', function (req, res) {
  res.send(zones);
})

app.put('/api/v1/updatescreen', function (req, res) {
  screenblank();
  res.send('OK');
})

app.listen(port, function () {
  console.log('RoonRest extension started, listening on port ' + port)
})
<commit_msg>New local zone ID<commit_after>// TODO:
// - Check success of Roon API calls
// - Volume control
// - Seek

// Parts of this code are copied from https://github.com/RoonLabs/roon-extension-powermate

"use strict";

var pjson = require('./package.json');
var package_version = pjson.version; //process.env.npm_package_version does not work under forever

var local_zone = '1601b2e08b11dfe59f49b0c472a871078257' // zone id which runs on the local server, for screen control

// Roon setup

var RoonApi = require("node-roon-api"),
  RoonApiSettings = require('node-roon-api-settings'),
  RoonApiStatus = require('node-roon-api-status'),
  RoonApiTransport = require('node-roon-api-transport');

var core;
var zones = [];

var roon = new RoonApi({
  extension_id: 'roonrest',
  display_name: 'Roon Rest Controller',
  display_version: package_version,
  publisher: 'Matthew Eckhaus',
  email: 'contact@roonlabs.com',
  website: 'https://github.com/matteck/roonrest',

  core_paired: function (core_) {
    core = core_;

    core.services.RoonApiTransport.subscribe_zones((response, msg) => {
      if (response == "Subscribed") {
        let curZones = msg.zones.reduce((p, e) => (p[e.zone_id] = e) && p, {});
        zones = curZones;
      } else if (response == "Changed") {
        var z;
        if (msg.zones_removed) msg.zones_removed.forEach(e => delete (zones[e.zone_id]));
        if (msg.zones_added) msg.zones_added.forEach(e => zones[e.zone_id] = e);
        if (msg.zones_changed) msg.zones_changed.forEach(e => zones[e.zone_id] = e);
      }
    });

  },
  core_unpaired: function (core_) {
    core = undefined;
  }
});

var mysettings = roon.load_config("settings") || {
  zone: null,
};

function makelayout(settings) {
  var l = {
    values: settings,
    layout: [],
    has_error: false
  };

  l.layout.push({
    type: "zone",
    title: "Zone",
    setting: "zone",
  });

  return l;
}

var svc_settings = new RoonApiSettings(roon, {
  get_settings: function (cb) {
    cb(makelayout(mysettings));
  },
  save_settings: function (req, isdryrun, settings) {
    let l = makelayout(settings.values);
    req.send_complete(l.has_error ? "NotValid" : "Success", { settings: l });

    if (!isdryrun && !l.has_error) {
      mysettings = l.values;
      svc_settings.update_settings(l);
      roon.save_config("settings", mysettings);
      update_status();
    }
  }
});

var svc_status = new RoonApiStatus(roon);

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function screenblank() {
  // Update dpms settings whenever play state changes on local machine
  // It's easier to check the state directly than trying to guess based on the action
  if (!local_zone) {
    return;
  }
  // Hack - Zone state doesn't update immediately
  await sleep(250);
  var roon_state = zones[local_zone]['state'];
  var xset = "/usr/bin/xset -display :0.0";
  var timeout = "600";
  const { exec } = require('child_process');
  var cmd = xset + ' q';
  console.log('Doing ' + cmd)
  exec(cmd, (err, xsetout, stderr) => {
    if (err) {
      console.log("xset q failed");
      console.log(stderr);
      return;
    } else {
      var found = xsetout.match(/DPMS is (\w+)/);
      var dpms_state = found[1];
      if (roon_state == "paused") {
        if (dpms_state == "Disabled") {
          var cmd = xset + ' +dpms';
          console.log('Doing ' + cmd)
          exec(cmd, (err, stdout, stderr) => {
            if (err) {
              console.log("xset +dpms failed");
              console.log(stderr);
              return;
            }
          });
        }
        // DPMS is on, make sure it has the right timeout
        var found = xsetout.match(/Standby:\s+\d+\s+Suspend:\s+\d+\s+Off:\s+(\d+)/);
        if (found[1] != timeout) {
          var cmd = xset + ' dpms ' + timeout + ' ' + timeout + ' ' + timeout;
          console.log('Doing ' + cmd);
          exec(cmd, (err, stdout, stderr) => {
            if (err) {
              console.log("xset dpms timeout failed");
              console.log(stderr);
              return;
            }
          });
        }
      } else if (roon_state == "playing" && dpms_state == "Enabled") {
        var cmd = xset + ' -dpms';
        console.log('Doing ' + cmd)
        exec(cmd, (err, stdout, stderr) => {
          if (err) {
            console.log("xset -dpms failed");
            console.log(stderr);
            return;
          }
        });
      }
    }
  });
}

roon.init_services({
  required_services: [RoonApiTransport],
  provided_services: [svc_settings, svc_status],
});

function update_status() {
  if (mysettings.hasOwnProperty("zone") && mysettings.zone != null && mysettings.zone.hasOwnProperty("name")) {
    svc_status.set_status("Ready. Attached to " + mysettings.zone.name, false);
  } else {
    svc_status.set_status("Loaded. No zone assigned");
  }
}

update_status();
roon.start_discovery();

// Express
var port = 3000

var express = require('express')
var app = express()

var not_registered_error = "The RoonRest extension is not enabled. Please enable it in Roon settings and try again.";

// Universal control actions
app.put('/api/v1/zone/all/control/:action(pause)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    console.log('Doing pause_all');
    core.services.RoonApiTransport.pause_all(screenblank);
    res.send('OK');
  }
})

// Zone-specific control actions
app.put('/api/v1/zone/:zone/control/:action(play|pause|playpause|stop|previous|next)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var action = req.params['action'];
    console.log('Doing action ' + action)
    let this_zone = null;
    if (req.params['zone'] == 'current')
      this_zone = mysettings.zone;
    else
      this_zone = zones[req.params['zone']];
    if (this_zone == null) {
      res.status('404').send();
    }
    core.services.RoonApiTransport.control(this_zone, action, screenblank);
    res.send('OK');
  }
})

// Settings
app.put('/api/v1/zone/:zone/settings/:name(shuffle|auto_radio)/:value(on|off)', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    console.log('Doing set ' + req.params['name'] + ' to ' + req.params['value']);
    var settings_object = {};
    var setting_name = req.params['name']
    if (req.params['value'] == 'on') {
      settings_object[setting_name] = 1;
    } else {
      settings_object[setting_name] = 0;
    }
    let this_zone = null;
    if (req.params['zone'] == 'current')
      this_zone = mysettings.zone;
    else
      this_zone = zones[req.params['zone']];
    core.services.RoonApiTransport.change_settings(this_zone, settings_object);
    res.send('OK');
  }
})

// Volume
app.put('/api/v1/zone/:zone/volume/:how(absolute|relative|relative_step)/:value', function (req, res) {
  if (core == undefined) {
    res.status('503').send(not_registered_error);
  } else {
    var how = req.params['how'];
    var value = req.params['value']
    console.log('volume how is ' + how);
    core.services.RoonApiTransport.change_volume(mysettings.zone, how, value);
    res.send('OK');
  }
})

app.get('/api/v1', function (req, res) {
  res.send('RoonRest extensions v1')
})

app.get('/api/v1/zones', function (req, res) {
  res.send(zones);
})

app.put('/api/v1/updatescreen', function (req, res) {
  screenblank();
  res.send('OK');
})

app.listen(port, function () {
  console.log('RoonRest extension started, listening on port ' + port)
})
<|endoftext|>


<commit_before>var _ = require('underscore'),
	$ = require('jquery'),
	React = require('react'),
	Field = require('../Field'),
	Note = require('../../components/Note'),
	Select = require('react-select');

var SUPPORTED_TYPES = ['image/gif', 'image/png', 'image/jpeg', 'image/bmp', 'image/x-icon', 'application/pdf', 'image/x-tiff', 'image/x-tiff', 'application/postscript', 'image/vnd.adobe.photoshop', 'image/svg+xml'];

module.exports = Field.create({

	displayName: 'CloudinaryImageField',

	fileFieldNode: function() {
		return this.refs.fileField.getDOMNode();
	},

	changeImage: function() {
		this.refs.fileField.getDOMNode().click();
	},

	getImageSource: function() {
		if (this.hasLocal()) {
			return this.state.localSource;
		} else if (this.hasExisting()) {
			return this.props.value.url;
		} else {
			return null;
		}
	},

	getImageURL: function() {
		if (!this.hasLocal() && this.hasExisting()) {
			return this.props.value.url;
		}
	},

	/**
	 * Reset origin and removal.
	 */
	undoRemove: function() {
		this.fileFieldNode().value = '';
		this.setState({
			removeExisting: false,
			localSource:    null,
			origin:         false,
			action:         null
		});
	},

	/**
	 * Check support for input files on input change.
	 */
	fileChanged: function (event) {
		var self = this;

		if (window.FileReader) {
			var files = event.target.files;
			_.each(files, function (f) {
				if (!_.contains(SUPPORTED_TYPES, f.type)) {
					self.removeImage();
					alert('Unsupported file type. Supported formats are: GIF, PNG, JPG, BMP, ICO, PDF, TIFF, EPS, PSD, SVG');
					return false;
				}

				var fileReader = new FileReader();
				fileReader.onload = function (e) {
					if (!self.isMounted()) return;
					self.setState({
						localSource: e.target.result,
						origin: 'local'
					});
				};
				fileReader.readAsDataURL(f);
			});
		} else {
			this.setState({
				origin: 'local'
			});
		}
	},

	/**
	 * If we have a local file added then remove it and reset the file field.
	 */
	removeImage: function (e) {
		var state = {
			localSource: null,
			origin: false
		};

		if (this.hasLocal()) {
			this.fileFieldNode().value = '';
		} else if (this.hasExisting()) {
			state.removeExisting = true;

			if (this.props.autoCleanup) {
				if (e.altKey) {
					state.action = 'reset';
				} else {
					state.action = 'delete';
				}
			} else {
				if (e.altKey) {
					state.action = 'delete';
				} else {
					state.action = 'reset';
				}
			}
		}

		this.setState(state);
	},

	/**
	 * Is the currently active image uploaded in this session?
	 */
	hasLocal: function() {
		return this.state.origin === 'local';
	},

	/**
	 * Do we have an image preview to display?
	 */
	hasImage: function() {
		return this.hasExisting() || this.hasLocal();
	},

	/**
	 * Do we have an existing file?
	 */
	hasExisting: function() {
		return !!this.props.value.url;
	},

	/**
	 * Render an image preview
	 */
	renderImagePreview: function() {
		var iconClassName;
		var className = 'image-preview';

		if (this.hasLocal()) {
			className += ' upload-pending';
			iconClassName = 'ion-upload upload-pending';
		} else if (this.state.removeExisting) {
			className += ' removed';
			iconClassName = 'delete-pending ion-close';
		}

		var body = [this.renderImagePreviewThumbnail()];
		if (iconClassName) body.push(
);

		var url = this.getImageURL();

		if (url) {
			body = {body};
		} else {
			body =
{body}
;
		}

		return
{body}
;
	},

	renderImagePreviewThumbnail: function() {
		return ;
	},

	/**
	 * Render image details - leave these out if we're uploading a local file or
	 * the existing file is to be removed.
	 */
	renderImageDetails: function (add) {
		var values = null;

		if (!this.hasLocal() && !this.state.removeExisting) {
			values = (

{this.props.value.url}

					{this.renderImageDimensions()}

			);
		}

		return (

				{values}
				{add}

		);
	},

	renderImageDimensions: function() {
		return
{this.props.value.width} x {this.props.value.height}
;
	},

	/**
	 * Render an alert.
	 *
	 *  - On a local file, output a "to be uploaded" message.
	 *  - On a cloudinary file, output a "from cloudinary" message.
	 *  - On removal of existing file, output a "save to remove" message.
	 */
	renderAlert: function() {
		if (this.hasLocal()) {
			return (

Image selected - save to upload

			);
		} else if (this.state.origin === 'cloudinary') {
			return (

Image selected from Cloudinary

			);
		} else if (this.state.removeExisting) {
			return (

Image {this.props.autoCleanup ? 'deleted' : 'removed'} - save to confirm

			);
		} else {
			return null;
		}
	},

	/**
	 * Output clear/delete/remove button.
	 *
	 *  - On removal of existing image, output "undo remove" button.
	 *  - Otherwise output Cancel/Delete image button.
	 */
	renderClearButton: function() {
		if (this.state.removeExisting) {
			return (
				<button type='button' className='btn btn-link btn-cancel btn-undo-image' onClick={this.undoRemove}>
					Undo Remove
				</button>
			);
		} else {
			var clearText;
			if (this.hasLocal()) {
				clearText = 'Cancel Upload';
			} else {
				clearText = (this.props.autoCleanup ? 'Delete Image' : 'Remove Image');
			}
			return (
				<button type='button' className='btn btn-link btn-cancel btn-delete-image' onClick={this.removeImage}>
					{clearText}
				</button>
			);
		}
	},

	renderFileField: function() {
		return <input ref='fileField' type='file' name={this.props.paths.upload} className='field-upload' onChange={this.fileChanged} />;
	},

	renderFileAction: function() {
		return <input type='hidden' name={this.props.paths.action} className='field-action' value={this.state.action} />;
	},

	renderImageToolbar: function() {
		return (

					<button type='button' onClick={this.changeImage} className='btn btn-default btn-upload-image'>
						{this.hasImage() ? 'Change' : 'Upload'} Image
					</button>
					{this.hasImage() && this.renderClearButton()}

				{this.props.select && this.renderImageSelect()}

		);
	},

	renderImageSelect: function() {
		var getOptions = function(input, callback) {
			$.get('/keystone/api/cloudinary/autocomplete', {
				dataType: 'json',
				data: {
					q: input
				}
			}, function (data) {
				var options = [];

				_.each(data.items, function (item) {
					options.push({
						value: item.public_id,
						label: item.public_id
					});
				});

				callback(null, {
					options: options,
					complete: true
				});
			});
		};

		return (

				<Select
					placeholder='Search for an image from Cloudinary ...'
					className='ui-select2-cloudinary'
					name={this.props.paths.select}
					id={'field_' + this.props.paths.select}
					asyncOptions={getOptions}
				/>

		);
	},

	renderUI: function() {
		var container = [],
			body = [],
			hasImage = this.hasImage(),
			fieldClassName = 'field-ui';

		if (hasImage) {
			fieldClassName += ' has-image';
		}

		if (this.shouldRenderField()) {
			if (hasImage) {
				container.push(this.renderImagePreview());
				container.push(this.renderImageDetails(this.renderAlert()));
			}

			body.push(this.renderImageToolbar());
		} else {
			if (hasImage) {
				container.push(this.renderImagePreview());
				container.push(this.renderImageDetails());
			} else {
				container.push(
no image
);
			}
		}

		return (

				{this.props.label}

				{this.renderFileField()}
				{this.renderFileAction()}

{container}

					{body}
					<Note note={this.props.note} />

		);
	}
});
<commit_msg>Fixed an issue where image field value was null and wasn't loading in the backend properly<commit_after>var _ = require('underscore'),
	$ = require('jquery'),
	React = require('react'),
	Field = require('../Field'),
	Note = require('../../components/Note'),
	Select = require('react-select');

var SUPPORTED_TYPES = ['image/gif', 'image/png', 'image/jpeg', 'image/bmp', 'image/x-icon', 'application/pdf', 'image/x-tiff', 'image/x-tiff', 'application/postscript', 'image/vnd.adobe.photoshop', 'image/svg+xml'];

module.exports = Field.create({

	displayName: 'CloudinaryImageField',

	fileFieldNode: function() {
		return this.refs.fileField.getDOMNode();
	},

	changeImage: function() {
		this.refs.fileField.getDOMNode().click();
	},

	getImageSource: function() {
		if (this.hasLocal()) {
			return this.state.localSource;
		} else if (this.hasExisting()) {
			return this.props.value.url;
		} else {
			return null;
		}
	},

	getImageURL: function() {
		if (!this.hasLocal() && this.hasExisting()) {
			return this.props.value.url;
		}
	},

	/**
	 * Reset origin and removal.
	 */
	undoRemove: function() {
		this.fileFieldNode().value = '';
		this.setState({
			removeExisting: false,
			localSource:    null,
			origin:         false,
			action:         null
		});
	},

	/**
	 * Check support for input files on input change.
	 */
	fileChanged: function (event) {
		var self = this;

		if (window.FileReader) {
			var files = event.target.files;
			_.each(files, function (f) {
				if (!_.contains(SUPPORTED_TYPES, f.type)) {
					self.removeImage();
					alert('Unsupported file type. Supported formats are: GIF, PNG, JPG, BMP, ICO, PDF, TIFF, EPS, PSD, SVG');
					return false;
				}

				var fileReader = new FileReader();
				fileReader.onload = function (e) {
					if (!self.isMounted()) return;
					self.setState({
						localSource: e.target.result,
						origin: 'local'
					});
				};
				fileReader.readAsDataURL(f);
			});
		} else {
			this.setState({
				origin: 'local'
			});
		}
	},

	/**
	 * If we have a local file added then remove it and reset the file field.
	 */
	removeImage: function (e) {
		var state = {
			localSource: null,
			origin: false
		};

		if (this.hasLocal()) {
			this.fileFieldNode().value = '';
		} else if (this.hasExisting()) {
			state.removeExisting = true;

			if (this.props.autoCleanup) {
				if (e.altKey) {
					state.action = 'reset';
				} else {
					state.action = 'delete';
				}
			} else {
				if (e.altKey) {
					state.action = 'delete';
				} else {
					state.action = 'reset';
				}
			}
		}

		this.setState(state);
	},

	/**
	 * Is the currently active image uploaded in this session?
	 */
	hasLocal: function() {
		return this.state.origin === 'local';
	},

	/**
	 * Do we have an image preview to display?
	 */
	hasImage: function() {
		return this.hasExisting() || this.hasLocal();
	},

	/**
	 * Do we have an existing file?
	 */
	hasExisting: function() {
		return (this.props.value && this.props.value.url);
	},

	/**
	 * Render an image preview
	 */
	renderImagePreview: function() {
		var iconClassName;
		var className = 'image-preview';

		if (this.hasLocal()) {
			className += ' upload-pending';
			iconClassName = 'ion-upload upload-pending';
		} else if (this.state.removeExisting) {
			className += ' removed';
			iconClassName = 'delete-pending ion-close';
		}

		var body = [this.renderImagePreviewThumbnail()];
		if (iconClassName) body.push(
);

		var url = this.getImageURL();

		if (url) {
			body = {body};
		} else {
			body =
{body}
;
		}

		return
{body}
;
	},

	renderImagePreviewThumbnail: function() {
		return ;
	},

	/**
	 * Render image details - leave these out if we're uploading a local file or
	 * the existing file is to be removed.
	 */
	renderImageDetails: function (add) {
		var values = null;

		if (!this.hasLocal() && !this.state.removeExisting) {
			values = (

{this.props.value.url}

					{this.renderImageDimensions()}

			);
		}

		return (

				{values}
				{add}

		);
	},

	renderImageDimensions: function() {
		return
{this.props.value.width} x {this.props.value.height}
;
	},

	/**
	 * Render an alert.
	 *
	 *  - On a local file, output a "to be uploaded" message.
	 *  - On a cloudinary file, output a "from cloudinary" message.
	 *  - On removal of existing file, output a "save to remove" message.
	 */
	renderAlert: function() {
		if (this.hasLocal()) {
			return (

Image selected - save to upload

			);
		} else if (this.state.origin === 'cloudinary') {
			return (

Image selected from Cloudinary

			);
		} else if (this.state.removeExisting) {
			return (

Image {this.props.autoCleanup ? 'deleted' : 'removed'} - save to confirm

			);
		} else {
			return null;
		}
	},

	/**
	 * Output clear/delete/remove button.
	 *
	 *  - On removal of existing image, output "undo remove" button.
	 *  - Otherwise output Cancel/Delete image button.
	 */
	renderClearButton: function() {
		if (this.state.removeExisting) {
			return (
				<button type='button' className='btn btn-link btn-cancel btn-undo-image' onClick={this.undoRemove}>
					Undo Remove
				</button>
			);
		} else {
			var clearText;
			if (this.hasLocal()) {
				clearText = 'Cancel Upload';
			} else {
				clearText = (this.props.autoCleanup ? 'Delete Image' : 'Remove Image');
			}
			return (
				<button type='button' className='btn btn-link btn-cancel btn-delete-image' onClick={this.removeImage}>
					{clearText}
				</button>
			);
		}
	},

	renderFileField: function() {
		return <input ref='fileField' type='file' name={this.props.paths.upload} className='field-upload' onChange={this.fileChanged} />;
	},

	renderFileAction: function() {
		return <input type='hidden' name={this.props.paths.action} className='field-action' value={this.state.action} />;
	},

	renderImageToolbar: function() {
		return (

					<button type='button' onClick={this.changeImage} className='btn btn-default btn-upload-image'>
						{this.hasImage() ? 'Change' : 'Upload'} Image
					</button>
					{this.hasImage() && this.renderClearButton()}

				{this.props.select && this.renderImageSelect()}

		);
	},

	renderImageSelect: function() {
		var getOptions = function(input, callback) {
			$.get('/keystone/api/cloudinary/autocomplete', {
				dataType: 'json',
				data: {
					q: input
				}
			}, function (data) {
				var options = [];

				_.each(data.items, function (item) {
					options.push({
						value: item.public_id,
						label: item.public_id
					});
				});

				callback(null, {
					options: options,
					complete: true
				});
			});
		};

		return (

				<Select
					placeholder='Search for an image from Cloudinary ...'
					className='ui-select2-cloudinary'
					name={this.props.paths.select}
					id={'field_' + this.props.paths.select}
					asyncOptions={getOptions}
				/>

		);
	},

	renderUI: function() {
		var container = [],
			body = [],
			hasImage = this.hasImage(),
			fieldClassName = 'field-ui';

		if (hasImage) {
			fieldClassName += ' has-image';
		}

		if (this.shouldRenderField()) {
			if (hasImage) {
				container.push(this.renderImagePreview());
				container.push(this.renderImageDetails(this.renderAlert()));
			}

			body.push(this.renderImageToolbar());
		} else {
			if (hasImage) {
				container.push(this.renderImagePreview());
				container.push(this.renderImageDetails());
			} else {
				container.push(
no image
);
			}
		}

		return (

				{this.props.label}

				{this.renderFileField()}
				{this.renderFileAction()}

{container}

					{body}
					<Note note={this.props.note} />

		);
	}
});
<|endoftext|>


<commit_before>var fs           = require('fs');
var util         = require('util');
var EventEmitter = require('events').EventEmitter;
var async        = require('async');
var debug        = require('debug')('rpi-gpio');
var Epoll        = require('epoll').Epoll;

var PATH = '/sys/class/gpio';
var PINS = {
    v1: {
        // 1: 3.3v
        // 2: 5v
        '3':  0,
        // 4: 5v
        '5':  1,
        // 6: ground
        '7':  4,
        '8':  14,
        // 9: ground
        '10': 15,
        '11': 17,
        '12': 18,
        '13': 21,
        // 14: ground
        '15': 22,
        '16': 23,
        // 17: 3.3v
        '18': 24,
        '19': 10,
        // 20: ground
        '21': 9,
        '22': 25,
        '23': 11,
        '24': 8,
        // 25: ground
        '26': 7
    },
    v2: {
        // 1: 3.3v
        // 2: 5v
        '3':  2,
        // 4: 5v
        '5':  3,
        // 6: ground
        '7':  4,
        '8':  14,
        // 9: ground
        '10': 15,
        '11': 17,
        '12': 18,
        '13': 27,
        // 14: ground
        '15': 22,
        '16': 23,
        // 17: 3.3v
        '18': 24,
        '19': 10,
        // 20: ground
        '21': 9,
        '22': 25,
        '23': 11,
        '24': 8,
        // 25: ground
        '26': 7,

        // Model B+ pins
        // 27: ID_SD
        // 28: ID_SC
        '29': 5,
        // 30: ground
        '31': 6,
        '32': 12,
        '33': 13,
        // 34: ground
        '35': 19,
        '36': 16,
        '37': 26,
        '38': 20,
        // 39: ground
        '40': 21
    }
};

function Gpio() {
    var currentPins;
    var exportedInputPins = {};
    var exportedOutputPins = {};
    var getPinForCurrentMode = getPinRpi;
    var pollers = {};

    this.DIR_IN   = 'in';
    this.DIR_OUT  = 'out';
    this.MODE_RPI = 'mode_rpi';
    this.MODE_BCM = 'mode_bcm';

    this.EDGE_NONE    = 'none';
    this.EDGE_RISING  = 'rising';
    this.EDGE_FALLING = 'falling';
    this.EDGE_BOTH    = 'both';

    /**
     * Set pin reference mode. Defaults to 'mode_rpi'.
     *
     * @param {string} mode Pin reference mode, 'mode_rpi' or 'mode_bcm'
     */
    this.setMode = function(mode) {
        if (mode === this.MODE_RPI) {
            getPinForCurrentMode = getPinRpi;
        } else if (mode === this.MODE_BCM) {
            getPinForCurrentMode = getPinBcm;
        } else {
            throw new Error('Cannot set invalid mode');
        }

        this.emit('modeChange', mode);
    };

    /**
     * Setup a channel for use as an input or output
     *
     * @param {number}   channel   Reference to the pin in the current mode's schema
     * @param {string}   direction The pin direction, either 'in' or 'out'
     * @param edge       edge Informs the GPIO chip if it needs to generate interrupts. Either 'none', 'rising', 'falling' or 'both'. Defaults to 'none'
     * @param {function} onSetup   Optional callback
     */
    this.setup = function(channel, direction, edge, onSetup /*err*/) {
        if (arguments.length === 2 && typeof direction == 'function') {
            onSetup = direction;
            direction = this.DIR_OUT;
            edge = this.EDGE_NONE;
        } else if (arguments.length === 3 && typeof edge == 'function') {
            onSetup = edge;
            edge = this.EDGE_NONE;
        }

        direction = direction || this.DIR_OUT;
        edge = edge || this.EDGE_NONE;
        onSetup = onSetup || function() {};

        if (!channel) {
            return process.nextTick(function() {
                onSetup(new Error('Channel must be a number'));
            });
        }

        if (direction !== this.DIR_IN && direction !== this.DIR_OUT) {
            return process.nextTick(function() {
                onSetup(new Error('Cannot set invalid direction'));
            });
        }

        if (edge !== this.EDGE_NONE && edge !== this.EDGE_RISING && edge !== this.EDGE_FALLING && edge !== this.EDGE_BOTH) {
            return process.nextTick(function() {
                onSetup(new Error('Cannot set invalid edge'));
            });
        }

        var pinForSetup;
        async.waterfall([
            setRaspberryVersion,
            function(next) {
                pinForSetup = getPinForCurrentMode(channel);
                if (!pinForSetup) {
                    return next(new Error('Channel ' + channel + ' does not map to a GPIO pin'));
                }
                debug('set up pin %d', pinForSetup);
                isExported(pinForSetup, next);
            },
            function(isExported, next) {
                if (isExported) {
                    return unexportPin(pinForSetup, next);
                }
                return next(null);
            },
            function(next) {
                exportPin(pinForSetup, next);
            },
            function(next) {
                setEdge(pinForSetup, edge, next);
            },
            function(next) {
                this.emit('export', channel);

                if (direction === this.DIR_IN) {
                    exportedInputPins[pinForSetup] = true;
                } else {
                    exportedOutputPins[pinForSetup] = true;
                }

                setDirection(pinForSetup, direction, next);
            }.bind(this),
            function(next) {
                listen(channel, function(readChannel) {
                    this.read(readChannel, function(err, value) {
                        debug(
                            'failed to read value after a change on channel %d',
                            readChannel
                        );
                        this.emit('change', readChannel, value);
                    });
                }.bind(this));
                next()
            }.bind(this)
        ], onSetup);
    };

    /**
     * Write a value to a channel
     *
     * @param {number}   channel The channel to write to
     * @param {boolean}  value   If true, turns the channel on, else turns off
     * @param {function} cb      Optional callback
     */
    this.write = this.output = function(channel, value, cb /*err*/) {
        var pin = getPinForCurrentMode(channel);

        if (!exportedOutputPins[pin]) {
            var message;
            if (exportedInputPins[pin]) {
                message = 'Pin has been exported for input so cannot be written to';
            } else {
                message = 'Pin has not been exported';
            }

            return process.nextTick(function() {
                cb(new Error(message));
            });
        }

        value = (!!value && value !== '0') ? '1' : '0';
        fs.writeFile(PATH + '/gpio' + pin + '/value', value, cb || function() {});
    };

    /**
     * Read a value from a channel
     *
     * @param {number}   channel The channel to read from
     * @param {function} cb      Callback which receives the channel's boolean value
     */
    this.read = this.input = function(channel, cb /*err,value*/) {
        var pin = getPinForCurrentMode(channel);

        if (!exportedInputPins[pin] && !exportedOutputPins[pin]) {
            return process.nextTick(function() {
                cb(new Error('Pin has not been exported'));
            });
        }

        fs.readFile(PATH + '/gpio' + pin + '/value', 'utf-8', function(err, data) {
            data = (data + '').trim() || '0';
            return cb(err, data === '1');
        });
    };

    /**
     * Unexport any pins setup by this module
     *
     * @param {function} cb Optional callback
     */
    this.destroy = function(cb) {
        var _this = this;
        var tasks = Object.keys(exportedOutputPins)
            .concat(Object.keys(exportedInputPins))
            .map(function(pin) {
                return function(done) {
                    removeListener(pin, pollers)
                    unexportPin(pin, done);
                }
            });

        async.parallel(tasks, cb);
    };

    /**
     * Reset the state of the module
     */
    this.reset = function() {
        exportedOutputPins = {};
        exportedInputPins = {};
        this.removeAllListeners();

        currentPins = undefined;
        getPinForCurrentMode = getPinRpi;
        pollers = {}
    };

    // Init
    EventEmitter.call(this);
    this.reset();

    // Private functions requring access to state
    function setRaspberryVersion(cb) {
        if (currentPins) {
            return cb(null);
        }

        fs.readFile('/proc/cpuinfo', 'utf8', function(err, data) {
            if (err) return cb(err);

            // Match the last 4 digits of the number following "Revision:"
            var match = data.match(/Revision\s*:\s*[0-9a-f]*([0-9a-f]{4})/);
            var revisionNumber = parseInt(match[1], 16);
            var pinVersion = (revisionNumber < 4) ? 'v1' : 'v2';

            debug(
                'seen hardware revision %d; using pin mode %s',
                revisionNumber,
                pinVersion
            );

            currentPins = PINS[pinVersion];

            return cb(null);
        });
    };

    function getPinRpi(channel) {
        return currentPins[channel] + '';
    };

    function getPinBcm(channel) {
        channel = parseInt(channel, 10);
        return [
            3,
            5,
            7,
            8,
            10,
            11,
            12,
            13,
            15,
            16,
            18,
            19,
            21,
            22,
            23,
            24,
            26,
            29,
            31,
            32,
            33,
            35,
            36,
            37,
            38,
            40
        ].indexOf(channel) !== -1 ? (channel + '') : null;
    };

    /**
     * Listen for interrupts on a channel
     *
     * @param {number}      channel The channel to watch
     * @param {function}    cb Callback which receives the channel's err
     */
    function listen(channel, onChange) {
        var pin = getPinForCurrentMode(channel);

        if (!exportedInputPins[pin] && !exportedOutputPins[pin]) {
            throw new Error('Channel %d has not been exported', channel);
        }

        createListener(channel, pin, pollers, onChange);
    };
}
util.inherits(Gpio, EventEmitter);

function setEdge(pin, edge, cb) {
    debug('set edge %s on pin %d', edge.toUpperCase(), pin);
    fs.writeFile(PATH + '/gpio' + pin + '/edge', edge, function(err) {
        if (cb) return cb(err);
    });
}

function setDirection(pin, direction, cb) {
    debug('set direction %s on pin %d', direction.toUpperCase(), pin);
    fs.writeFile(PATH + '/gpio' + pin + '/direction', direction, function(err) {
        if (cb) return cb(err);
    });
}

function exportPin(pin, cb) {
    debug('export pin %d', pin);
    fs.writeFile(PATH + '/export', pin, function(err) {
        if (cb) return cb(err);
    });
}

function unexportPin(pin, cb) {
    debug('unexport pin %d', pin);
    fs.writeFile(PATH + '/unexport', pin, function(err) {
        if (cb) return cb(err);
    });
}

function isExported(pin, cb) {
    fs.exists(PATH + '/gpio' + pin, function(exists) {
        return cb(null, exists);
    });
}

function createListener(channel, pin, pollers, onChange) {
    debug('listen for pin %d', pin);
    var poller = new Epoll(function(err, innerfd, events) {
        if (err) throw err
        clearInterrupt(innerfd);
        onChange(channel);
    });

    var fd = fs.openSync(PATH + '/gpio' + pin + '/value', 'r+');
    clearInterrupt(fd);
    poller.add(fd, Epoll.EPOLLPRI);
    // Append ready-to-use remove function
    pollers[pin] = function() {
        poller.remove(fd).close();
    }
}

function removeListener(pin, pollers) {
    if (!pollers[pin]) {
        return
    }
    debug('remove listener for pin %d', pin)
    pollers[pin]()
    delete pollers[pin]
}

function clearInterrupt(fd) {
    fs.readSync(fd, new Buffer(1), 0, 1, 0);
}

module.exports = new Gpio;
<commit_msg>Fix incorrect debug message during change event<commit_after>var fs           = require('fs');
var util         = require('util');
var EventEmitter = require('events').EventEmitter;
var async        = require('async');
var debug        = require('debug')('rpi-gpio');
var Epoll        = require('epoll').Epoll;

var PATH = '/sys/class/gpio';
var PINS = {
    v1: {
        // 1: 3.3v
        // 2: 5v
        '3':  0,
        // 4: 5v
        '5':  1,
        // 6: ground
        '7':  4,
        '8':  14,
        // 9: ground
        '10': 15,
        '11': 17,
        '12': 18,
        '13': 21,
        // 14: ground
        '15': 22,
        '16': 23,
        // 17: 3.3v
        '18': 24,
        '19': 10,
        // 20: ground
        '21': 9,
        '22': 25,
        '23': 11,
        '24': 8,
        // 25: ground
        '26': 7
    },
    v2: {
        // 1: 3.3v
        // 2: 5v
        '3':  2,
        // 4: 5v
        '5':  3,
        // 6: ground
        '7':  4,
        '8':  14,
        // 9: ground
        '10': 15,
        '11': 17,
        '12': 18,
        '13': 27,
        // 14: ground
        '15': 22,
        '16': 23,
        // 17: 3.3v
        '18': 24,
        '19': 10,
        // 20: ground
        '21': 9,
        '22': 25,
        '23': 11,
        '24': 8,
        // 25: ground
        '26': 7,

        // Model B+ pins
        // 27: ID_SD
        // 28: ID_SC
        '29': 5,
        // 30: ground
        '31': 6,
        '32': 12,
        '33': 13,
        // 34: ground
        '35': 19,
        '36': 16,
        '37': 26,
        '38': 20,
        // 39: ground
        '40': 21
    }
};

function Gpio() {
    var currentPins;
    var exportedInputPins = {};
    var exportedOutputPins = {};
    var getPinForCurrentMode = getPinRpi;
    var pollers = {};

    this.DIR_IN   = 'in';
    this.DIR_OUT  = 'out';
    this.MODE_RPI = 'mode_rpi';
    this.MODE_BCM = 'mode_bcm';

    this.EDGE_NONE    = 'none';
    this.EDGE_RISING  = 'rising';
    this.EDGE_FALLING = 'falling';
    this.EDGE_BOTH    = 'both';

    /**
     * Set pin reference mode. Defaults to 'mode_rpi'.
     *
     * @param {string} mode Pin reference mode, 'mode_rpi' or 'mode_bcm'
     */
    this.setMode = function(mode) {
        if (mode === this.MODE_RPI) {
            getPinForCurrentMode = getPinRpi;
        } else if (mode === this.MODE_BCM) {
            getPinForCurrentMode = getPinBcm;
        } else {
            throw new Error('Cannot set invalid mode');
        }

        this.emit('modeChange', mode);
    };

    /**
     * Setup a channel for use as an input or output
     *
     * @param {number}   channel   Reference to the pin in the current mode's schema
     * @param {string}   direction The pin direction, either 'in' or 'out'
     * @param edge       edge Informs the GPIO chip if it needs to generate interrupts. Either 'none', 'rising', 'falling' or 'both'. Defaults to 'none'
     * @param {function} onSetup   Optional callback
     */
    this.setup = function(channel, direction, edge, onSetup /*err*/) {
        if (arguments.length === 2 && typeof direction == 'function') {
            onSetup = direction;
            direction = this.DIR_OUT;
            edge = this.EDGE_NONE;
        } else if (arguments.length === 3 && typeof edge == 'function') {
            onSetup = edge;
            edge = this.EDGE_NONE;
        }

        direction = direction || this.DIR_OUT;
        edge = edge || this.EDGE_NONE;
        onSetup = onSetup || function() {};

        if (!channel) {
            return process.nextTick(function() {
                onSetup(new Error('Channel must be a number'));
            });
        }

        if (direction !== this.DIR_IN && direction !== this.DIR_OUT) {
            return process.nextTick(function() {
                onSetup(new Error('Cannot set invalid direction'));
            });
        }

        if (edge !== this.EDGE_NONE && edge !== this.EDGE_RISING && edge !== this.EDGE_FALLING && edge !== this.EDGE_BOTH) {
            return process.nextTick(function() {
                onSetup(new Error('Cannot set invalid edge'));
            });
        }

        var pinForSetup;
        async.waterfall([
            setRaspberryVersion,
            function(next) {
                pinForSetup = getPinForCurrentMode(channel);
                if (!pinForSetup) {
                    return next(new Error('Channel ' + channel + ' does not map to a GPIO pin'));
                }
                debug('set up pin %d', pinForSetup);
                isExported(pinForSetup, next);
            },
            function(isExported, next) {
                if (isExported) {
                    return unexportPin(pinForSetup, next);
                }
                return next(null);
            },
            function(next) {
                exportPin(pinForSetup, next);
            },
            function(next) {
                setEdge(pinForSetup, edge, next);
            },
            function(next) {
                this.emit('export', channel);

                if (direction === this.DIR_IN) {
                    exportedInputPins[pinForSetup] = true;
                } else {
                    exportedOutputPins[pinForSetup] = true;
                }

                setDirection(pinForSetup, direction, next);
            }.bind(this),
            function(next) {
                listen(channel, function(readChannel) {
                    this.read(readChannel, function(err, value) {
                        if (err) {
                            debug(
                                'Error reading channel value after change, %d',
                                readChannel
                            );
                            return
                        }
                        this.emit('change', readChannel, value);
                    });
                }.bind(this));
                next()
            }.bind(this)
        ], onSetup);
    };

    /**
     * Write a value to a channel
     *
     * @param {number}   channel The channel to write to
     * @param {boolean}  value   If true, turns the channel on, else turns off
     * @param {function} cb      Optional callback
     */
    this.write = this.output = function(channel, value, cb /*err*/) {
        var pin = getPinForCurrentMode(channel);

        if (!exportedOutputPins[pin]) {
            var message;
            if (exportedInputPins[pin]) {
                message = 'Pin has been exported for input so cannot be written to';
            } else {
                message = 'Pin has not been exported';
            }

            return process.nextTick(function() {
                cb(new Error(message));
            });
        }

        value = (!!value && value !== '0') ? '1' : '0';
        fs.writeFile(PATH + '/gpio' + pin + '/value', value, cb || function() {});
    };

    /**
     * Read a value from a channel
     *
     * @param {number}   channel The channel to read from
     * @param {function} cb      Callback which receives the channel's boolean value
     */
    this.read = this.input = function(channel, cb /*err,value*/) {
        var pin = getPinForCurrentMode(channel);

        if (!exportedInputPins[pin] && !exportedOutputPins[pin]) {
            return process.nextTick(function() {
                cb(new Error('Pin has not been exported'));
            });
        }

        fs.readFile(PATH + '/gpio' + pin + '/value', 'utf-8', function(err, data) {
            data = (data + '').trim() || '0';
            return cb(err, data === '1');
        });
    };

    /**
     * Unexport any pins setup by this module
     *
     * @param {function} cb Optional callback
     */
    this.destroy = function(cb) {
        var _this = this;
        var tasks = Object.keys(exportedOutputPins)
            .concat(Object.keys(exportedInputPins))
            .map(function(pin) {
                return function(done) {
                    removeListener(pin, pollers)
                    unexportPin(pin, done);
                }
            });

        async.parallel(tasks, cb);
    };

    /**
     * Reset the state of the module
     */
    this.reset = function() {
        exportedOutputPins = {};
        exportedInputPins = {};
        this.removeAllListeners();

        currentPins = undefined;
        getPinForCurrentMode = getPinRpi;
        pollers = {}
    };

    // Init
    EventEmitter.call(this);
    this.reset();

    // Private functions requring access to state
    function setRaspberryVersion(cb) {
        if (currentPins) {
            return cb(null);
        }

        fs.readFile('/proc/cpuinfo', 'utf8', function(err, data) {
            if (err) return cb(err);

            // Match the last 4 digits of the number following "Revision:"
            var match = data.match(/Revision\s*:\s*[0-9a-f]*([0-9a-f]{4})/);
            var revisionNumber = parseInt(match[1], 16);
            var pinVersion = (revisionNumber < 4) ? 'v1' : 'v2';

            debug(
                'seen hardware revision %d; using pin mode %s',
                revisionNumber,
                pinVersion
            );

            currentPins = PINS[pinVersion];

            return cb(null);
        });
    };

    function getPinRpi(channel) {
        return currentPins[channel] + '';
    };

    function getPinBcm(channel) {
        channel = parseInt(channel, 10);
        return [
            3,
            5,
            7,
            8,
            10,
            11,
            12,
            13,
            15,
            16,
            18,
            19,
            21,
            22,
            23,
            24,
            26,
            29,
            31,
            32,
            33,
            35,
            36,
            37,
            38,
            40
        ].indexOf(channel) !== -1 ? (channel + '') : null;
    };

    /**
     * Listen for interrupts on a channel
     *
     * @param {number}      channel The channel to watch
     * @param {function}    cb Callback which receives the channel's err
     */
    function listen(channel, onChange) {
        var pin = getPinForCurrentMode(channel);

        if (!exportedInputPins[pin] && !exportedOutputPins[pin]) {
            throw new Error('Channel %d has not been exported', channel);
        }

        createListener(channel, pin, pollers, onChange);
    };
}
util.inherits(Gpio, EventEmitter);

function setEdge(pin, edge, cb) {
    debug('set edge %s on pin %d', edge.toUpperCase(), pin);
    fs.writeFile(PATH + '/gpio' + pin + '/edge', edge, function(err) {
        if (cb) return cb(err);
    });
}

function setDirection(pin, direction, cb) {
    debug('set direction %s on pin %d', direction.toUpperCase(), pin);
    fs.writeFile(PATH + '/gpio' + pin + '/direction', direction, function(err) {
        if (cb) return cb(err);
    });
}

function exportPin(pin, cb) {
    debug('export pin %d', pin);
    fs.writeFile(PATH + '/export', pin, function(err) {
        if (cb) return cb(err);
    });
}

function unexportPin(pin, cb) {
    debug('unexport pin %d', pin);
    fs.writeFile(PATH + '/unexport', pin, function(err) {
        if (cb) return cb(err);
    });
}

function isExported(pin, cb) {
    fs.exists(PATH + '/gpio' + pin, function(exists) {
        return cb(null, exists);
    });
}

function createListener(channel, pin, pollers, onChange) {
    debug('listen for pin %d', pin);
    var poller = new Epoll(function(err, innerfd, events) {
        if (err) throw err
        clearInterrupt(innerfd);
        onChange(channel);
    });

    var fd = fs.openSync(PATH + '/gpio' + pin + '/value', 'r+');
    clearInterrupt(fd);
    poller.add(fd, Epoll.EPOLLPRI);
    // Append ready-to-use remove function
    pollers[pin] = function() {
        poller.remove(fd).close();
    }
}

function removeListener(pin, pollers) {
    if (!pollers[pin]) {
        return
    }
    debug('remove listener for pin %d', pin)
    pollers[pin]()
    delete pollers[pin]
}

function clearInterrupt(fd) {
    fs.readSync(fd, new Buffer(1), 0, 1, 0);
}

module.exports = new Gpio;
<|endoftext|>


<commit_before>import { identity } from 'lodash';
import { extendReducer } from './extendReducer';
import { nullReducer } from './nullReducer';

/**
 * Creates a reducer wrapper which return the payload of the action for the
 * given action type.
 * @param {string} actionType
 * @param {function} mapResult - function to be invoked with payload as argument
 *   before returning it.
 * @returns {function} a function of signature (reducer) => newReducer
 */
export const withPayloadReducer = (actionType, mapResult = identity) => (reducer = nullReducer) =>
  extendReducer(reducer, {
    [actionType]: (_, { payload } = {}) => mapResult(payload)
  });
<commit_msg>import individual function from lodash<commit_after>import identity from 'lodash/identity';
import { extendReducer } from './extendReducer';
import { nullReducer } from './nullReducer';

/**
 * Creates a reducer wrapper which return the payload of the action for the
 * given action type.
 * @param {string} actionType
 * @param {function} mapResult - function to be invoked with payload as argument
 *   before returning it.
 * @returns {function} a function of signature (reducer) => newReducer
 */
export const withPayloadReducer = (actionType, mapResult = identity) => (reducer = nullReducer) =>
  extendReducer(reducer, {
    [actionType]: (_, { payload } = {}) => mapResult(payload)
  });
<|endoftext|>


<commit_before>'use strict';

module.exports = function(driver, connectorSpec) {
	connectorSpec.shouldBehaveLikeMusicSite(driver, {
		url: 'https://listen.moe/',
		playButtonSelector: '.player-icon'
	});
};
<commit_msg>Fix Listen.moe test<commit_after>'use strict';

module.exports = function(driver, connectorSpec) {
	connectorSpec.shouldBehaveLikeMusicSite(driver, {
		url: 'https://listen.moe/',
		playButtonSelector: '.icon-music-play'
	});
};
<|endoftext|>