diff --git a/README.md b/README.md index 051612f..f7ed761 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Each zone source combo has its own switch and brightness. This allows for siri control of your nuvo whole house audio system. One thing of note is that 100 Volume is not enabled (it sets it to 24%) so that when the home app turns a zone on, it will not blast music at you at full volume. -Sample Config: +## Sample Config: ``` "platforms": [ @@ -18,10 +18,13 @@ Sample Config: } ] ``` -Options: +### Options: The port option is for you to set the path to the nuvo. If you are using a usb to rs232 adapter cable with a pl2303 or something similar, then run the command ```dmesg | grep -i usb``` and find a line like ```usb 3-2: pl2303 converter now attached to ttyUSB0```. The now attached to ????? should go into the config like ```"port": "/dev/?????"```. The numZones option if for you to set the number of zones that your amplifier supports for zone detection purposes. The portRetryInterval option is the number of seconds to wait before retrying the serialport. If set to 0 or not set at all, it will not retry the connection. + +## nuvo-config (optional) +`nuvo-config` is an optional config tool installed with the plugin for configuration of the Nuvo Grand Concerto without using the Windows GUI. diff --git a/dist/cli.js b/dist/cli.js new file mode 100644 index 0000000..74dc022 --- /dev/null +++ b/dist/cli.js @@ -0,0 +1,311 @@ +#!/usr/bin/env node +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const readline_1 = __importDefault(require("readline")); +const MAX_ZONES = 20; +const MAX_SOURCES = 6; +const MAX_GAIN = 14; +const MAX_EIGHTTEEN = 18; +const winston_1 = require("winston"); +const customLevels = { + levels: { + error: 0, + warn: 1, + info: 2, + nuvo: 3, + verbose: 4, + debug: 5, + silly: 6 + }, + colors: { + error: 'red', + warn: 'orange', + info: 'green', + nuvo: 'blue', + verbose: 'yellow', + debug: 'grey', + silly: 'black' + } +}; +const logger = winston_1.createLogger({ + levels: customLevels.levels, + level: 'nuvo', + format: winston_1.format.combine(winston_1.format.colorize({ all: true }), winston_1.format.printf(info => { + const { level, message, ...args } = info; + return `${Object.keys(args).length ? JSON.stringify(args, null, 2) : message}`; + })), + transports: [ + new winston_1.transports.Console() + ] +}); +winston_1.addColors(customLevels.colors); +var serialConnection; +var serial = require("./serial"); +var port = '/dev/tty.usbserial'; +var numZones = 8; +var rl; +function printShellHelp() { + logger.info("Usage:"); + logger.info(" nuvo-config "); +} +function printCommandHelp(cmd) { + if (cmd) + logger.info(`\`${cmd}\` was not called with the correct arguments\n`); + logger.info("Nuvo Config runs in:"); + logger.info(" Sentence Mode: string together commands to quickly change setup"); + logger.info(" ex. `zone 4 enable name \"Best Zone\"` - enables and renames zone 4"); + logger.info(" ex. `source 3 disable` - disables source 3\n"); + logger.info(" `help` - print this help text"); + logger.info(" `exit` - exit nuvo-config\n"); + logger.info("Available Commands:"); + logger.info(" `source ` - set the source that config actions will apply to\n (1-20) (0 to apply to all)"); + logger.info(" `zone ` - set the zone that config actions will apply to\n (1-6) (0 to apply to all)"); + logger.info(" `status` - get the current config status of the zone or source"); + logger.info(" `enable` - enable the selected zone or source"); + logger.info(" `disable` - disable the selected zone or source"); + logger.info(" `name ` - set the name of a selected zone or soure\n"); + logger.info(" `shortname ` - set the three letter shortname for a source"); + logger.info(" `gain ` - set the gain value for a source (0 - 14)\n"); + logger.info(" `eq` - get the current eq config for a zone"); + logger.info(" `bass ` - set the bass value for a zone (-18 - 18)"); + logger.info(" `treble ` - set the treble value for a zone (-18 - 18)"); + logger.info(" `balance ` - set the left/right balance value for a zone (-18 - 18)"); + logger.info(" `loudcomp (enable | disable)` - enable/disable loudness compensation"); +} +function argSplitString(data) { + let singleQCount = false; + let doubleQCount = false; + let escapeNext = false; + let args = []; + let currentArg = ""; + for (var c of data) { + if (escapeNext) { + currentArg += c; + escapeNext = false; + } + else if (c === '\"') { + doubleQCount = !doubleQCount; + } + else if (c === '\'') { + singleQCount = !singleQCount; + } + else if (c === "\\") { + escapeNext = true; + } + else if (c === ' ' && !singleQCount && !doubleQCount) { + args.push(currentArg); + currentArg = ""; + } + else { + currentArg += c; + } + } + args.push(currentArg); + currentArg = ""; + return args; +} +var args = process.argv.slice(2); +// logger.info("\nPort:", port, "NumZones:", numZones); +class CLIPlatform { + constructor() { + this.cli = 1; + this.outstandingCmds = 0; + } + prompt() { + rl.prompt(); + } + onPortOpen() { + launchConsole(); + } +} +function checkZoneCommand(zone, callback) { + if (zone === -1) { + logger.info("Please specify a valid zone"); + cli.prompt(); + } + else if (zone === 0) { + for (let z = 1; z <= MAX_ZONES; z++) { + cli.outstandingCmds++; + callback(z); + } + } + else { + cli.outstandingCmds++; + callback(zone); + } +} +function checkSourceCommand(source, callback) { + if (source === -1) { + logger.info("Please specify a valid source"); + cli.prompt(); + } + else if (source === 0) { + for (let s = 1; s <= MAX_SOURCES; s++) { + cli.outstandingCmds++; + callback(s); + } + } + else { + cli.outstandingCmds++; + callback(source); + } +} +var cmdQueue = []; +function hasNext() { + return cmdQueue.length > 0; +} +function getNext(word) { + if (hasNext()) + return cmdQueue.shift(); + else + logger.info(`Please enter the appropriate arguments for ${word}`); +} +/** +* Min Exclusive, Max Inclusive, number grabbing +*/ +function getNumber(min, max, step, word) { + if (hasNext()) { + let next = getNext(word); + let val = parseInt(next); + if (isNaN(val) || val <= min || val > max) { + logger.info(`Please enter a valid ${word} number between ${min + 1} and ${max}`); + cmdQueue.unshift(next); + return min; + } + else { + if (val % step != 0) { + val -= val % step; + } + return val; + } + } + else { + return min; + } +} +function parseConfigLine(line) { + cmdQueue = argSplitString(line); + let zone = -1; + let source = -1; + let zoneMode = false; + while (hasNext()) { + let current = getNext(); + // Commands switch + switch (current) { + case 'zone': + zone = getNumber(-1, MAX_ZONES, 1, 'zone'); + zoneMode = true; + break; + case 'source': + source = getNumber(-1, MAX_SOURCES, 1, 'zone'); + zoneMode = false; + break; + case 'enable': + if (zoneMode) + checkZoneCommand(zone, (zone) => { serialConnection.zoneConfigEnable(zone, 1); }); + else + checkSourceCommand(source, (source) => { serialConnection.sourceConfigEnable(source, 1); }); + break; + case 'disable': + if (zoneMode) + checkZoneCommand(zone, (zone) => { serialConnection.zoneConfigEnable(zone, 0); }); + else + checkSourceCommand(source, (source) => { serialConnection.sourceConfigEnable(source, 0); }); + break; + case 'name': + let name = getNext('name'); + if (name != null) { + if (zoneMode) + checkZoneCommand(zone, (zone) => { serialConnection.zoneConfigName(zone, name); }); + else + checkSourceCommand(source, (source) => { serialConnection.sourceConfigName(source, name); }); + } + break; + case 'shortname': + let shortname = getNext('shortname'); + if (shortname != null) { + checkSourceCommand(source, (source) => { serialConnection.sourceConfigShortName(source, shortname); }); + } + break; + case 'gain': + let gain = getNumber(-1, MAX_GAIN, 1, 'gain'); + if (gain != -1) + checkSourceCommand(source, (source) => { serialConnection.sourceConfigGain(source, gain); }); + break; + case 'status': + if (zoneMode) + checkZoneCommand(zone, (zone) => { serialConnection.zoneAskConfig(zone); }); + else + checkSourceCommand(source, (source) => { serialConnection.sourceAskConfig(source); }); + break; + case 'eq': + checkZoneCommand(zone, (zone) => { serialConnection.zoneAskEQ(zone); }); + break; + case 'bass': + let bass = getNumber(-MAX_EIGHTTEEN - 1, MAX_EIGHTTEEN, 2, 'bass'); + if (bass != -MAX_EIGHTTEEN - 1) + checkZoneCommand(zone, (zone) => { serialConnection.zoneConfigBass(zone, bass); }); + break; + case 'treble': + let treble = getNumber(-MAX_EIGHTTEEN - 1, MAX_EIGHTTEEN, 2, 'treble'); + if (treble != -MAX_EIGHTTEEN - 1) + checkZoneCommand(zone, (zone) => { serialConnection.zoneConfigTreble(zone, treble); }); + break; + case 'balance': + let bal = getNumber(-MAX_EIGHTTEEN - 1, MAX_EIGHTTEEN, 2, 'balance'); + if (bal != -MAX_EIGHTTEEN - 1) + checkZoneCommand(zone, (zone) => { serialConnection.zoneConfigBalance(zone, bal); }); + break; + case 'loudcomp': + let lc = getNext('loudcomp'); + if (lc === "enable") + checkZoneCommand(zone, (zone) => { serialConnection.zoneConfigLoudComp(zone, 1); }); + else + checkZoneCommand(zone, (zone) => { serialConnection.zoneConfigLoudComp(zone, 0); }); + break; + default: + logger.info(`\`${current}\` is not a registered nuvo-config command`); + printCommandHelp(); + cli.prompt(); + } + } +} +function launchConsole() { + rl = readline_1.default.createInterface({ + input: process.stdin, + output: process.stdout, + prompt: 'Nuvo-Config> ' + }); + rl.prompt(); + rl.on('line', (line) => { + switch (line.trim()) { + case 'help': + printCommandHelp(); + cli.prompt(); + break; + default: + parseConfigLine(line); + break; + case 'exit': + case 'quit': + console.info('Bye!'); + process.exit(0); + } + }).on('close', () => { + logger.info('Bye!'); + process.exit(0); + }); +} +if (args.length > 0) { + port = args.shift(); + var cli = new CLIPlatform(); + logger.info("Beginning serial connection"); + serialConnection = new serial.NuvoSerial(logger, port, numZones, 0, cli); +} +else { + printShellHelp(); +} +//# sourceMappingURL=cli.js.map \ No newline at end of file diff --git a/dist/platform.js b/dist/platform.js new file mode 100644 index 0000000..28f5319 --- /dev/null +++ b/dist/platform.js @@ -0,0 +1,155 @@ +"use strict"; +const PLUGIN_NAME = "homebridge-nuvo"; +const PLATFORM_NAME = "nuvo-platform"; +let hap; +let Accessory; +var serial; +class NuvoPlatform { + constructor(log, config, api) { + this.log = log; + this.api = api; + if (config.port) { + this.port = config.port; + } + else { + this.log.warn("Currently using a default port path. You should consider figuring out what your port is so this will actually work."); + this.port = '/dev/tty.usbserial'; + } + if (config.numZones) { + this.numZones = config.numZones; + this.numZones = config.numZones; + } + else { + this.numZones = 8; + } + if (config.portRetryInterval) { + this.portRetryInterval = config.portRetryInterval * 1000; + } + else { + this.portRetryInterval = 0; + } + serial = require("./serial"); + this.zoneSourceCombo = new Array(this.numZones + 1); + const sourceArrayLength = serial.MAX_SOURCES + 1; + for (var i = 1; i < this.zoneSourceCombo.length; i++) { + this.zoneSourceCombo[i] = new Array(sourceArrayLength); + } + this.zoneConfigs = new Array(this.numZones + 1); + this.sourceConfigs = new Array(sourceArrayLength); + api.on("didFinishLaunching" /* DID_FINISH_LAUNCHING */, () => { + this.serialConnection = new serial.NuvoSerial(this.log, this.port, this.numZones, this.portRetryInterval, this); + }); + } + // Make sure this works and doesn't break a promise + configureAccessory(accessory) { + this.log.debug(`adding ${accessory.context.zone}, ${accessory.context.source}`); + accessory.getService(hap.Service.AccessoryInformation) + .setCharacteristic(hap.Characteristic.Manufacturer, "Will MacCormack") + .setCharacteristic(hap.Characteristic.Model, "Nuvo Speaker") + .setCharacteristic(hap.Characteristic.SerialNumber, "NVGC") + .setCharacteristic(hap.Characteristic.FirmwareRevision, "2.0.0"); + const onChar = accessory.getService(hap.Service.Lightbulb).getCharacteristic(hap.Characteristic.On); + onChar.on("set" /* SET */, (value, callback) => { + if (value === true) { + this.serialConnection.zoneOn(accessory.context.zone); + this.serialConnection.zoneSource(accessory.context.zone, accessory.context.source); + } + else { + this.serialConnection.zoneOff(accessory.context.zone); + } + this.serialConnection.zoneAskStatus(accessory.context.zone); + callback(undefined, value); + }); + onChar.on("get" /* GET */, (callback) => { + this.serialConnection.zoneAskStatus(accessory.context.zone); + callback(); + }); + const brightChar = accessory.getService(hap.Service.Lightbulb).getCharacteristic(hap.Characteristic.Brightness); + brightChar.on("get" /* GET */, (callback) => { + this.serialConnection.zoneAskStatus(accessory.context.zone); + callback(); + }); + brightChar.on("set" /* SET */, (value, callback) => { + if (value === 100) { + var vol = 59; + } + else { + var vol = Math.round((Number(value) * (79 / 100) - 79) * -1); + } + this.serialConnection.zoneVolume(accessory.context.zone, vol); + callback(undefined, value); + }); + this.zoneSourceCombo[accessory.context.zone][accessory.context.source] = accessory; + } + addAccessory(zoneNum, sourceNum) { + if (!this.zoneSourceCombo[zoneNum][sourceNum]) { + const accessoryName = this.zoneConfigs[zoneNum][2].substring(5, (this.zoneConfigs[zoneNum][2].length - 1)) + + " " + this.sourceConfigs[sourceNum][2].substring(5, (this.sourceConfigs[sourceNum][2].length - 1)); + this.log.debug(accessoryName); + const accessoryUUID = hap.uuid.generate(accessoryName + zoneNum + sourceNum); + this.log.debug(accessoryUUID); + const accessory = new Accessory(accessoryName, accessoryUUID); + accessory.context.zone = zoneNum; + accessory.context.source = sourceNum; + accessory.addService(hap.Service.Lightbulb, accessoryName); + this.configureAccessory(accessory); + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + } + } + addZone(zone, zoneCfg) { + this.zoneConfigs[zone] = zoneCfg; + // code to add all sources for this zone + if (this.zoneConfigs[zone][1] === "ENABLE1") { + for (var source = 1; source <= serial.MAX_SOURCES; source++) { + if (this.sourceConfigs[source] && this.sourceConfigs[source][1] === "ENABLE1") { + this.addAccessory(zone, source); + } + } + } + } + addSource(source, sourceCfg) { + this.sourceConfigs[source] = sourceCfg; + if (this.sourceConfigs[source][1] === "ENABLE1") { + for (var zone = 1; zone <= this.numZones; zone++) { + if (this.zoneConfigs[zone] && this.zoneConfigs[zone][1] === "ENABLE1") { + this.addAccessory(zone, source); + } + } + } + } + updateZone(zoneNum, zoneStatus) { + // code to update all things for that zone + let sourceOn = 0; + if (zoneStatus[1] === "ON") { + sourceOn = Number(zoneStatus[2].substring(3)); + } + let vol = "VOL79"; + if (zoneStatus[3]) { + vol = zoneStatus[3]; + } + if (vol === "MUTE") { + var volume = 0; + } + else { + var vnum = (parseInt(vol.substring(3))); + var volume = Math.round(((vnum * -1) + 79) * (100 / 79)); + } + for (var source = 1; source <= serial.MAX_SOURCES; source++) { + if (this.zoneSourceCombo[zoneNum][source]) { + var lightService = this.zoneSourceCombo[zoneNum][source].getService(hap.Service.Lightbulb); + if (lightService.getCharacteristic(hap.Characteristic.On).value != (source === sourceOn)) { + lightService.updateCharacteristic(hap.Characteristic.On, (source === sourceOn)); + } + if (lightService.getCharacteristic(hap.Characteristic.Brightness).value != volume) { + lightService.updateCharacteristic(hap.Characteristic.Brightness, volume); + } + } + } + } +} +module.exports = (api) => { + hap = api.hap; + Accessory = api.platformAccessory; + api.registerPlatform(PLATFORM_NAME, NuvoPlatform); +}; +//# sourceMappingURL=platform.js.map \ No newline at end of file diff --git a/dist/serial.js b/dist/serial.js new file mode 100644 index 0000000..4e4f6c3 --- /dev/null +++ b/dist/serial.js @@ -0,0 +1,268 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.NuvoSerial = exports.MAX_SOURCES = void 0; +let SerialPort = require('serialport'); +let Readline = require('@serialport/parser-readline'); +exports.MAX_SOURCES = 6; +class NuvoSerial { + constructor(log, portPath, numZones, portRetryInterval, platform) { + this.log = log; + this.portPath = portPath; + this.numZones = numZones; + this.portRetryInterval = portRetryInterval; + this.platform = platform; + this.port = new SerialPort(portPath, { + baudRate: 57600, + autoOpen: false + }); + this.openPort(); + } + openPort() { + this.port.open((err) => { + if (err) { + if (this.portRetryInterval > 0) { + this.log.info(`That serial port didn't open right now. I will try again in ${this.portRetryInterval / 1000} second(s).`); + setTimeout(this.openPort.bind(this), this.portRetryInterval); + } + else { + this.log.error("That port does not seem to exist (or this process doesn't have access to it)."); + if (!this.platform.cli) + this.log.error("Consider changing the port or adding a port retry interval in the config.json file for homebridge."); + else + this.log.error("Consider specifying a port with -p ."); + } + } + else { + this.log.info("Port setup process seems to have worked. Yay!"); + this.parser = this.port.pipe(new Readline({ delimiter: '\r\n' })); + if (!this.platform.cli) + this.startTimers(); + else { + setTimeout(this.sort.bind(this), 10); + setTimeout(this.platform.onPortOpen, 50); + } + } + }); + } + //Zone functions + zoneOn(zone) { + this.port.write(`*Z${zone}ON\r`); + this.log.debug(`*Z${zone}ON\r`); + } + zoneOff(zone) { + this.port.write(`*Z${zone}OFF\r`); + this.log.debug(`*Z${zone}OFF\r`); + } + zoneSource(zone, source) { + this.port.write(`*Z${zone}SRC${source}\r`); + this.log.debug(`*Z${zone}SRC${source}\r`); + } + zoneVolume(zone, volume) { + this.port.write(`*Z${zone}VOL${volume}\r`); + this.log.debug(`*Z${zone}VOL${volume}\r`); + } + zoneMuteOn(zone) { + this.port.write(`*Z${zone}MUTEON\r`); + this.log.debug(`*Z${zone}MUTEON\r`); + } + zoneMuteOff(zone) { + this.port.write(`*Z${zone}MUTEOFF\r`); + this.log.debug(`*Z${zone}MUTEOFF\r`); + } + allOff() { + this.port.write(`*ALLOFF\r`); + this.log.debug(`*ALLOFF\r`); + } + // Source config functions + sourceConfigName(source, name) { + this.port.write(`*SCFG${source}NAME\"${name}\"\r`); + this.log.debug(`*SCFG${source}NAME\"${name}\"\r`); + } + sourceConfigShortName(source, name) { + this.port.write(`*SCFG${source}SHORTNAME\"${name}\"\r`); + this.log.debug(`*SCFG${source}SHORTNAME\"${name}\"\r`); + } + sourceConfigEnable(source, enable) { + this.port.write(`*SCFG${source}ENABLE${enable}\r`); + this.log.debug(`*SCFG${source}ENABLE${enable}\r`); + } + sourceConfigGain(source, gain) { + this.port.write(`*SCFG${source}GAIN\"${gain}\"\r`); + this.log.debug(`*SCFG${source}GAIN\"${gain}\"\r`); + } + sourceConfigNuvonet(source, nuvonet) { + this.port.write(`*SCFG${source}NUVONET${nuvonet}\r`); + this.log.debug(`*SCFG${source}NUVONET${nuvonet}\r`); + } + // Zone Config functions + zoneConfigEnable(zone, enable) { + this.port.write(`*ZCFG${zone}ENABLE${enable}\r`); + this.log.debug(`*ZCFG${zone}ENABLE${enable}\r`); + } + zoneConfigName(zone, name) { + this.port.write(`*ZCFG${zone}NAME\"${name}\"\r`); + this.log.debug(`*ZCFG${zone}NAME\"${name}\"\r`); + } + zoneConfigBass(zone, bass) { + this.port.write(`*ZCFG${zone}BASS${bass}\r`); + this.log.debug(`*ZCFG${zone}BASS${bass}\r`); + } + zoneConfigTreble(zone, treble) { + this.port.write(`*ZCFG${zone}TREB${treble}\r`); + this.log.debug(`*ZCFG${zone}TREB${treble}\r`); + } + zoneConfigBalance(zone, balance) { + if (balance < 0) { + this.port.write(`*ZCFG${zone}BALR${balance * -1}\r`); + this.log.debug(`*ZCFG${zone}BALR${balance * -1}\r`); + } + else if (balance === 0) { + this.port.write(`*ZCFG${zone}BALC\r`); + this.log.debug(`*ZCFG${zone}BALC\r`); + } + else { + this.port.write(`*ZCFG${zone}BALL${balance}\r`); + this.log.debug(`*ZCFG${zone}BALL${balance}\r`); + } + } + zoneConfigLoudComp(zone, enable) { + this.port.write(`*ZCFG${zone}LOUDCMP${enable}\r`); + this.log.debug(`*ZCFG${zone}LOUDCMP${enable}\r`); + } + //Status functions + zoneAskStatus(zone) { + this.port.write(`*Z${zone}STATUS?\r`); + this.log.debug(`*Z${zone}STATUS?\r`); + } + allZoneStatus() { + for (var i = 1; i <= this.numZones; i++) { + var delay = ((i) * 50); + setTimeout(this.zoneAskStatus.bind(this), delay, i); + } + } + zoneAskConfig(zone) { + this.port.write(`*ZCFG${zone}STATUS?\r`); + this.log.debug(`*ZCFG${zone}STATUS?\r`); + } + zoneAskEQ(zone) { + this.port.write(`*ZCFG${zone}EQ?\r`); + this.log.debug(`*ZCFG${zone}EQ?\r`); + } + allZoneConfig() { + for (var i = 1; i <= this.numZones; i++) { + var delay = ((i) * 50); + setTimeout(this.zoneAskConfig.bind(this), delay, i); + } + } + sourceAskConfig(source) { + this.port.write(`*SCFG${source}STATUS?\r`); + this.log.debug(`*SCFG${source}STATUS?\r`); + } + allSourceConfig() { + for (var i = 1; i <= exports.MAX_SOURCES; i++) { + var delay = ((i) * 50); + setTimeout(this.sourceAskConfig.bind(this), delay, i); + } + } + statusCheck(seconds) { + var interval = seconds * 1000; + setInterval((log, allZoneStatus) => { + log.debug("I am checking every " + seconds + " seconds."); + allZoneStatus(); + }, interval, this.log, this.allZoneStatus.bind(this)); + } + listen(callback) { + this.port.open(() => { + this.parser.on('data', function (data) { + if (data.trim() !== '') { + return callback(data); + } + }); + }); + } + sort() { + this.listen((data) => { + var parts = data.split(","); + if (!this.platform.cli) { + if ("#Z" === parts[0].substring(0, 2)) { + if ("#ZCFG" === parts[0].substring(0, 5)) { + let zone = parseInt(parts[0].substring(5)); + this.platform.addZone(zone, parts); + } + else { + let zone = parseInt(parts[0].substring(2)); + this.platform.updateZone(zone, parts); + } + } + else if ("#SCFG" === parts[0].substring(0, 5)) { + let source = parseInt(parts[0].substring(5)); + this.platform.addSource(source, parts); + } + } + else { + let formatted = {}; + if ("#ZCFG" === parts[0].substring(0, 5)) { + formatted['zone'] = parseInt(parts[0].substring(5)); + if (parts[1].substring(0, 6) === "ENABLE") { + if (parts[1].substring(6) === "1") { + formatted['enabled'] = true; + formatted['name'] = parts[2].substring(5, parts[2].length - 1); + // formatted['linkedTo'] = parts[3].substring(7); + // formatted['group'] = parts[4].substring(5); + // formatted['sourcesEnabled'] = parseInt(parts[5].substring(7)); + } + else { + formatted['enabled'] = false; + } + } + else { + formatted['bass'] = parseInt(parts[1].substring(4)); + formatted['treble'] = parseInt(parts[2].substring(4)); + if (parts[3].substring(3, 4) === "C") { + formatted['balance'] = 0; + } + else { + formatted['balance'] = parseInt(((parts[3].substring(3, 4) === "R") ? '+' : '-') + parts[3].substring(4)); + } + formatted['loudcomp'] = parts[4].substring(7) === "1"; + } + this.log.log('nuvo', formatted); + this.platform.outstandingCmds--; + if (this.platform.outstandingCmds <= 0) { + this.platform.prompt(); + this.platform.outstandingCmds = 0; + } + } + else if ("#SCFG" === parts[0].substring(0, 5)) { + formatted['source'] = parts[0].substring(5); + if (parts[1].substring(6) === "1") { + formatted['enabled'] = true; + formatted['name'] = parts[2].substring(5, parts[2].length - 1); + formatted['gain'] = parseInt(parts[3].substring(4)); + formatted['nuvonet'] = parts[4].substring(7) === "1"; + formatted['shortname'] = parts[5].substring(10, 13); + } + else { + formatted['enabled'] = false; + } + this.log.log('nuvo', formatted); + this.platform.outstandingCmds--; + if (this.platform.outstandingCmds <= 0) { + this.platform.prompt(); + this.platform.outstandingCmds = 0; + } + } + } + }); + } + startTimers() { + this.log.debug("Starting the timers "); + setTimeout(this.sort.bind(this), 1500); + setTimeout(this.allSourceConfig.bind(this), 2000); + setTimeout(this.allZoneConfig.bind(this), 3500); + setTimeout(this.allZoneStatus.bind(this), 5000); + this.statusCheck(300); + } +} +exports.NuvoSerial = NuvoSerial; +//# sourceMappingURL=serial.js.map \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5e28b9d..42d943f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,9 +1,20 @@ { "name": "homebridge-nuvo", - "version": "2.1.0", + "version": "2.2.0", "lockfileVersion": 1, "requires": true, - "dependencies": { "@serialport/binding-abstract": { + "dependencies": { + "@dabh/diagnostics": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.2.tgz", + "integrity": "sha512-+A1YivoVDNNVCdfozHSR8v/jyuuLTMXwjWuxPFlFlUapXoGc+Gj9mDlTDDfrwl7rXCl2tNZ0kE8sIBO6YOn96Q==", + "requires": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "@serialport/binding-abstract": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/@serialport/binding-abstract/-/binding-abstract-9.0.1.tgz", "integrity": "sha512-ncUFSRyVdpyCRuah2dzrs99UfEWWMAhV31ae2FT6j4f8TypQ8OgAF8KkcHiD4M3wORDh3UKCCTS7n8aJWge1RA==", @@ -120,6 +131,11 @@ "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", "dev": true }, + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, "available-typed-arrays": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.2.tgz", @@ -238,6 +254,30 @@ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, + "color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/color/-/color-3.0.0.tgz", + "integrity": "sha512-jCpd5+s0s0t7p3pHQKpnJ0TpQKKdleP71LWcA0aqiljpiuAkOSUFN/dyH8ZwF0hRmFlrIuRhufds1QyEP9EB+w==", + "requires": { + "color-convert": "^1.9.1", + "color-string": "^1.5.2" + }, + "dependencies": { + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" + } + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -250,8 +290,30 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.4.tgz", + "integrity": "sha512-57yF5yt8Xa3czSEW1jfQDE79Idk0+AkN/4KWad6tbdxUmAs3MvjxlWSWD4deYytcRfoZ9nhKyFl1kj5tBvidbw==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "colors": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", + "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" + }, + "colorspace": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.2.tgz", + "integrity": "sha512-vt+OoIP2d76xLhjwbBaucYlNSpPsrJWPlBTtwCpQKIu6/CSMutyzX93O/Do0qzpH3YoHEes8YEFXyZ797rEhzQ==", + "requires": { + "color": "3.0.x", + "text-hex": "1.0.x" + } }, "commander": { "version": "5.1.0", @@ -362,6 +424,11 @@ "safe-buffer": "^5.1.1" } }, + "enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -429,17 +496,32 @@ "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, "fast-srp-hap": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.2.tgz", "integrity": "sha512-wABhZRrFhlovqJQ1HygOUB4R6WZW2hmlpvVYh2dVCy8BPLabDrB/Tu6XI3B4QfmhtHk8s1OeiFqJHY7FBsphug==", "dev": true }, + "fecha": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.0.tgz", + "integrity": "sha512-aN3pcx/DSmtyoovUudctc8+6Hl4T+hI9GBBHLjA76jdZl7+b1sgh5g4k+u/GL3dTy1/pnYzKp69FpJ0OicE3Wg==" + }, "file-uri-to-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" }, + "fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, "foreach": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", @@ -621,6 +703,11 @@ "call-bind": "^1.0.0" } }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "is-bigint": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.1.tgz", @@ -689,6 +776,11 @@ "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", "dev": true }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + }, "is-string": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", @@ -756,6 +848,23 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "logform": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.2.0.tgz", + "integrity": "sha512-N0qPlqfypFx7UHNn4B3lzS/b0uLqt2hmuoa+PpuXNYgozdJYAyauF5Ky0BWVjrxDlMWiT3qN4zPq3vVAfZy7Yg==", + "requires": { + "colors": "^1.2.1", + "fast-safe-stringify": "^2.0.4", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "triple-beam": "^1.3.0" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -914,6 +1023,14 @@ "wrappy": "1" } }, + "one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "requires": { + "fn.name": "1.x.x" + } + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1075,6 +1192,14 @@ "simple-concat": "^1.0.0" } }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -1091,6 +1216,11 @@ "source-map": "^0.6.0" } }, + "stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=" + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -1186,12 +1316,22 @@ } } }, + "text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, "thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "triple-beam": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.3.0.tgz", + "integrity": "sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -1292,6 +1432,43 @@ "string-width": "^1.0.2 || 2" } }, + "winston": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.3.3.tgz", + "integrity": "sha512-oEXTISQnC8VlSAKf1KYSSd7J6IWuRPQqDdo8eoRNaYKLvwSb5+79Z3Yi1lrl6KDpU6/VWaxpakDAtb1oQ4n9aw==", + "requires": { + "@dabh/diagnostics": "^2.0.2", + "async": "^3.1.0", + "is-stream": "^2.0.0", + "logform": "^2.2.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.4.0" + }, + "dependencies": { + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + } + } + }, + "winston-transport": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.4.0.tgz", + "integrity": "sha512-Lc7/p3GtqtqPBYYtS6KCN3c77/2QCev51DvcJKbkFPQNoj1sinkGwLGFDxkXY9J6p9+EPnYs+D90uwbnaiURTw==", + "requires": { + "readable-stream": "^2.3.7", + "triple-beam": "^1.2.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/package.json b/package.json index fdbc43a..582c048 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homebridge-nuvo", - "version": "2.1.0", + "version": "2.2.0", "main": "dist/platform.js", "scripts": { "clean": "rimraf ./dist", @@ -24,7 +24,8 @@ }, "dependencies": { "@serialport/parser-readline": ">=2.0.2", - "serialport": ">=7.1.5" + "serialport": ">=7.1.5", + "winston": "^3.3.3" }, "devDependencies": { "@types/node": "10.17.19", @@ -33,12 +34,12 @@ "homebridge": ">=1.0.4" }, "description": "This is a homebridge plugin for that allows serial control of nuvo whole home audio systems.", - "bin" : { - "nuvo-config" : "./dist/cli.js" + "bin": { + "nuvo-config": "./dist/cli.js" }, "funding": { - "type" : "paypal", - "url" : "https://paypal.me/WillMacCormack" + "type": "paypal", + "url": "https://paypal.me/WillMacCormack" }, "repository": { "type": "git", diff --git a/src/cli.ts b/src/cli.ts index 648c02d..9253c64 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -5,6 +5,53 @@ import { import readline from 'readline'; const MAX_ZONES = 20; +const MAX_SOURCES = 6; +const MAX_GAIN = 14; +const MAX_EIGHTTEEN = 18; + +import { addColors, createLogger, format, transports } from 'winston'; + +const customLevels = { +levels: { + error: 0, + warn: 1, + info: 2, + nuvo: 3, + verbose: 4, + debug: 5, + silly: 6 +}, +colors: { + error: 'red', + warn: 'orange', + info: 'green', + nuvo: 'blue', + verbose: 'yellow', + debug: 'grey', + silly: 'black' +} + +}; + +const logger = createLogger({ + levels: customLevels.levels, + level: 'nuvo', + format: format.combine( + format.colorize({all: true}), + format.printf(info => { + const { + level, message, ...args + } = info; + + return `${Object.keys(args).length ? JSON.stringify(args, null, 2) : message}`; + }), + ), + transports: [ + new transports.Console() + ] +}); + +addColors(customLevels.colors); var serialConnection: NuvoSerial; var serial = require("./serial"); @@ -12,43 +59,41 @@ var serial = require("./serial"); var port = '/dev/tty.usbserial'; var numZones = 8; -var options = ['{-p|--port} ', '{-h|--help}']; - var rl: readline.Interface; function printShellHelp(): void { - console.log("\nUsage:"); - console.log(" nuvo-config ") - console.log(" nuvo-config []\n") - // console.log(" can be one of:") - // for (var command of commands) { - // console.log(` ${command}`) - // } - console.log("\n can be one of:"); - for (var option of options) { - console.log(` ${option}`) - } + logger.info("Usage:"); + logger.info(" nuvo-config ") } function printCommandHelp(cmd?: string): void { if (cmd) - console.log(`\`${cmd}\` was not called with the correct arguments\n`); - console.log("Nuvo Config runs in:"); - console.log(" Sentence Mode: string together commands to quickly change setup") - console.log(" ex. `zone 4 enable name \"Best Zone\"` - enables and renames zone 4") - console.log(" ex. `source 3 disable` - disables source 3\n") - // console.log(" Wizard Mode: walks you through the setup process") - // console.log(" ex. `zone 4 wizard` - launches the wizard for zone 4") - // console.log(" ex. `wizard` - launches the wizard for all zones and sources\n") - console.log("Available Commands:") - console.log(" `source ` - set the source that config actions will apply to") - console.log(" `zone ` - set the zone that config actions will apply to") - console.log(" `status` - get the current config status of the zone or source") - console.log(" `enable` - enable the selected zone or source") - console.log(" `disable` - disable the selected zone or source") - console.log(" `name ` - set the name of a selected zone or soure") - console.log(" `shortname ` - set the three letter shortname for a source") - console.log(" `gain ` - set the gain value for a source (0-14)") + logger.info(`\`${cmd}\` was not called with the correct arguments\n`); + logger.info("Nuvo Config runs in:"); + logger.info(" Sentence Mode: string together commands to quickly change setup"); + logger.info(" ex. `zone 4 enable name \"Best Zone\"` - enables and renames zone 4"); + logger.info(" ex. `source 3 disable` - disables source 3\n"); + + logger.info(" `help` - print this help text"); + logger.info(" `exit` - exit nuvo-config\n"); + logger.info("Available Commands:"); + logger.info(" `source ` - set the source that config actions will apply to\n (1-20) (0 to apply to all)"); + logger.info(" `zone ` - set the zone that config actions will apply to\n (1-6) (0 to apply to all)"); + logger.info(" `status` - get the current config status of the zone or source"); + logger.info(" `enable` - enable the selected zone or source"); + logger.info(" `disable` - disable the selected zone or source"); + logger.info(" `name ` - set the name of a selected zone or soure\n"); + + logger.info(" `shortname ` - set the three letter shortname for a source"); + logger.info(" `gain ` - set the gain value for a source (0 - 14)\n"); + + logger.info(" `eq` - get the current eq config for a zone"); + logger.info(" `bass ` - set the bass value for a zone (-18 - 18)"); + logger.info(" `treble ` - set the treble value for a zone (-18 - 18)"); + logger.info(" `balance ` - set the left/right balance value for a zone (-18 - 18)"); + logger.info(" `loudcomp (enable | disable)` - enable/disable loudness compensation"); + + } function argSplitString(data: string): string[] { @@ -61,20 +106,21 @@ function argSplitString(data: string): string[] { for (var c of data) { - if (escapeNext) + if (escapeNext) { currentArg += c; - else if (c == '\"') + escapeNext = false; + } else if (c === '\"') { doubleQCount = !doubleQCount; - else if (c == '\'') + } else if (c === '\'') { singleQCount = !singleQCount; - else if (c == "\\") + } else if (c === "\\") { escapeNext = true; - else if (c == ' ' && !singleQCount && !doubleQCount) { + } else if (c === ' ' && !singleQCount && !doubleQCount) { args.push(currentArg); currentArg = ""; - } - else + } else { currentArg += c; + } } args.push(currentArg); currentArg = ""; @@ -84,38 +130,20 @@ function argSplitString(data: string): string[] { var args: string[] = process.argv.slice(2); -while (args.length > 0) { - let current: string = args.shift(); - - if (args.length > 0 && current.substring(0, 1) === "-") - { - // Options switch - switch (current) { - case '-p': - case '--port': - port = args.shift(); - break; - case '-h': - case '--help': - default: - console.log(current, "is not a registered nuvo-config option"); - args.shift(); - printShellHelp(); - } - } -} -// console.log("\nPort:", port, "NumZones:", numZones); +// logger.info("\nPort:", port, "NumZones:", numZones); class CLIPlatform { cli: number; + outstandingCmds: number; constructor() { this.cli = 1; + this.outstandingCmds = 0; } - prompt() { + prompt () { rl.prompt(); } @@ -124,122 +152,163 @@ class CLIPlatform } } -function checkZoneCommand(zone: number, callback) { - if (zone != -1) +function checkZoneCommand(zone: number, callback: any) { + if (zone === -1) { + logger.info("Please specify a valid zone"); + cli.prompt(); + } else if (zone === 0) { + for (let z=1; z<=MAX_ZONES; z++) { + cli.outstandingCmds++; + callback(z); + } + } else { + cli.outstandingCmds++; callback(zone); - else - console.log("Please specify a valid zone"); + } } -function checkSourceCommand(source: number, callback) { - if (source != -1) +function checkSourceCommand(source: number, callback: any) { + if (source === -1) { + logger.info("Please specify a valid source"); + cli.prompt(); + } else if (source === 0) { + for (let s=1; s<=MAX_SOURCES; s++) { + cli.outstandingCmds++; + callback(s); + } + } else { + cli.outstandingCmds++; callback(source); + } +} + +var cmdQueue = []; + +function hasNext() { + return cmdQueue.length > 0; +} + + +function getNext(word?: string) { + if (hasNext()) + return cmdQueue.shift(); else - console.log("Please specify a valid source"); + logger.info(`Please enter the appropriate arguments for ${word}`); + +} + +/** +* Min Exclusive, Max Inclusive, number grabbing +*/ +function getNumber(min: number, max: number, step: number, word: string) { + if (hasNext()) + { + let next = getNext(word); + let val = parseInt(next); + if (isNaN(val) || val <= min || val > max) { + logger.info(`Please enter a valid ${word} number between ${min+1} and ${max}`); + cmdQueue.unshift(next); + return min; + } else { + if (val % step != 0) { + val -= val % step; + } + return val + } + } else { + return min; + } } function parseConfigLine(line: string):void { - let cmdQueue = argSplitString(line); + cmdQueue = argSplitString(line); let zone = -1; let source = -1; let zoneMode = false; - while (cmdQueue.length > 0) { - let current: string = cmdQueue.shift(); + while (hasNext()) { + let current: string = getNext(); // Commands switch switch (current) { case 'zone': - if (cmdQueue.length > 0) { - let val = parseInt(cmdQueue.shift()); - if (isNaN(val) || val <= 0 || val > MAX_ZONES) - console.log("Please enter a valid zone number after the word zone"); - else - zone = val; - zoneMode = true; - } else { - console.log(`\`${current}\` was not called with the correct arguments`); - printCommandHelp(); - } + zone = getNumber(-1, MAX_ZONES, 1, 'zone'); + zoneMode = true; break; case 'source': - if (cmdQueue.length > 0) { - let val = parseInt(cmdQueue.shift()); - - if (isNaN(val)) - console.log("Please enter a valid source number after the word source"); - else - source = val - zoneMode = false; - } else { - printCommandHelp(current); - } + source = getNumber(-1, MAX_SOURCES, 1, 'zone'); + zoneMode = false; break; case 'enable': if (zoneMode) - checkZoneCommand(zone, (zone) => {serialConnection.zoneConfigEnable(zone, 1)}); + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneConfigEnable(zone, 1)}); else - checkSourceCommand(source, (source) => {serialConnection.sourceConfigEnable(source, 1)}); + checkSourceCommand(source, (source: number) => {serialConnection.sourceConfigEnable(source, 1)}); break; case 'disable': if (zoneMode) - checkZoneCommand(zone, (zone) => {serialConnection.zoneConfigEnable(zone, 0)}); + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneConfigEnable(zone, 0)}); else - checkSourceCommand(source, (source) => {serialConnection.sourceConfigEnable(source, 0)}); + checkSourceCommand(source, (source: number) => {serialConnection.sourceConfigEnable(source, 0)}); break; case 'name': - if (cmdQueue.length > 0) - { - let name = cmdQueue.shift(); + let name = getNext('name'); + if (name != null) { if (zoneMode) - checkZoneCommand(zone, (zone) => {serialConnection.zoneConfigName(zone, name)}); + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneConfigName(zone, name)}); else - checkSourceCommand(source, (source) => {serialConnection.sourceConfigName(source, name)}); - } - else - { - printCommandHelp(current); + checkSourceCommand(source, (source: number) => {serialConnection.sourceConfigName(source, name)}); } break; case 'shortname': - if (cmdQueue.length > 0) - { - let name = cmdQueue.shift(); + let shortname = getNext('shortname'); - checkSourceCommand(source, (source) => {serialConnection.sourceConfigShortName(source, name)}); - } - else - { - printCommandHelp(current); + if (shortname != null) { + checkSourceCommand(source, (source: number) => {serialConnection.sourceConfigShortName(source, shortname)}); } break; case 'gain': - if (cmdQueue.length > 0) - { - let val = parseInt(cmdQueue.shift()); - - if (isNaN(val)) - console.log("Please enter a valid gain value after the word gain"); - else - checkSourceCommand(source, (source) => {serialConnection.sourceConfigGain(source, val)}); - } - else - { - printCommandHelp(current); - } + let gain = getNumber(-1, MAX_GAIN, 1, 'gain'); + if (gain != -1) + checkSourceCommand(source, (source: number) => {serialConnection.sourceConfigGain(source, gain)}); break; case 'status': if (zoneMode) - checkZoneCommand(zone, (zone) => {serialConnection.zoneAskConfig(zone)}); + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneAskConfig(zone)}); + else + checkSourceCommand(source, (source: number) => {serialConnection.sourceAskConfig(source)}); + break; + case 'eq': + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneAskEQ(zone)}); + break; + case 'bass': + let bass = getNumber(-MAX_EIGHTTEEN-1, MAX_EIGHTTEEN, 2, 'bass'); + if (bass != -MAX_EIGHTTEEN-1) + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneConfigBass(zone, bass)}); + break; + case 'treble': + let treble = getNumber(-MAX_EIGHTTEEN-1, MAX_EIGHTTEEN, 2, 'treble'); + if (treble != -MAX_EIGHTTEEN-1) + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneConfigTreble(zone, treble)}); + break; + case 'balance': + let bal = getNumber(-MAX_EIGHTTEEN-1, MAX_EIGHTTEEN, 2, 'balance'); + if (bal != -MAX_EIGHTTEEN-1) + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneConfigBalance(zone, bal)}); + break; + case 'loudcomp': + let lc = getNext('loudcomp'); + if (lc === "enable") + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneConfigLoudComp(zone, 1)}); else - checkSourceCommand(source, (source) => {serialConnection.sourceAskConfig(source)}); + checkZoneCommand(zone, (zone: number) => {serialConnection.zoneConfigLoudComp(zone, 0)}); break; default: - console.log(`\`${current}\` is not a registered nuvo-config command`); - - printCommandHelp(); + logger.info(`\`${current}\` is not a registered nuvo-config command`); + printCommandHelp(); + cli.prompt(); } } } @@ -255,30 +324,30 @@ function launchConsole():void { rl.on('line', (line: string) => { switch (line.trim()) { - case 'hello': - console.log('world!'); - break; case 'help': printCommandHelp(); + cli.prompt(); break; default: parseConfigLine(line); break; case 'exit': case 'quit': - console.log('Bye!'); + console.info('Bye!'); process.exit(0); } - rl.prompt(); }).on('close', () => { - console.log('Bye!'); + logger.info('Bye!'); process.exit(0); }); } +if (args.length > 0) { + port = args.shift(); -let cli: CLIPlatform = new CLIPlatform(); - -serialConnection = new serial.NuvoSerial(console, port, numZones, 0, cli); - -// launchConsole(); + var cli: CLIPlatform = new CLIPlatform(); + logger.info("Beginning serial connection"); + serialConnection = new serial.NuvoSerial(logger, port, numZones, 0, cli); +} else { + printShellHelp(); +} diff --git a/src/platform.ts b/src/platform.ts index 8db0eb3..4706d4c 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -143,7 +143,7 @@ class NuvoPlatform implements DynamicPlatformPlugin { brightChar.on(CharacteristicEventTypes.SET, (value: CharacteristicValue, callback: CharacteristicSetCallback) => { - if (value == 100) + if (value === 100) { var vol = 59; } @@ -235,7 +235,7 @@ class NuvoPlatform implements DynamicPlatformPlugin { vol = zoneStatus[3]; } - if (vol == "MUTE") + if (vol === "MUTE") { var volume = 0; } else { @@ -249,9 +249,9 @@ class NuvoPlatform implements DynamicPlatformPlugin { { var lightService = this.zoneSourceCombo[zoneNum][source].getService(hap.Service.Lightbulb); - if (lightService.getCharacteristic(hap.Characteristic.On).value != (source == sourceOn)) + if (lightService.getCharacteristic(hap.Characteristic.On).value != (source === sourceOn)) { - lightService.updateCharacteristic(hap.Characteristic.On, (source == sourceOn)); + lightService.updateCharacteristic(hap.Characteristic.On, (source === sourceOn)); } if (lightService.getCharacteristic(hap.Characteristic.Brightness).value != volume) diff --git a/src/serial.ts b/src/serial.ts index 836c312..41bc728 100644 --- a/src/serial.ts +++ b/src/serial.ts @@ -47,7 +47,10 @@ export class NuvoSerial else { this.log.error("That port does not seem to exist (or this process doesn't have access to it)."); - this.log.error("Consider changing the port or adding a port retry interval in the config.json file for homebridge."); + if (!this.platform.cli) + this.log.error("Consider changing the port or adding a port retry interval in the config.json file for homebridge."); + else + this.log.error("Consider specifying a port with -p ."); } } else @@ -58,8 +61,8 @@ export class NuvoSerial this.startTimers(); else { - setTimeout(this.sort.bind(this), 500); - setTimeout(this.platform.onPortOpen, 600); + setTimeout(this.sort.bind(this), 10); + setTimeout(this.platform.onPortOpen, 50); } } }); @@ -157,6 +160,38 @@ export class NuvoSerial this.log.debug(`*ZCFG${zone}NAME\"${name}\"\r`); } + zoneConfigBass(zone: number, bass: number) + { + this.port.write(`*ZCFG${zone}BASS${bass}\r`); + this.log.debug(`*ZCFG${zone}BASS${bass}\r`); + } + + zoneConfigTreble(zone: number, treble: number) + { + this.port.write(`*ZCFG${zone}TREB${treble}\r`); + this.log.debug(`*ZCFG${zone}TREB${treble}\r`); + } + + zoneConfigBalance(zone: number, balance: number) + { + if (balance < 0) { + this.port.write(`*ZCFG${zone}BALR${balance*-1}\r`); + this.log.debug(`*ZCFG${zone}BALR${balance*-1}\r`); + } else if (balance === 0) { + this.port.write(`*ZCFG${zone}BALC\r`); + this.log.debug(`*ZCFG${zone}BALC\r`); + } else { + this.port.write(`*ZCFG${zone}BALL${balance}\r`); + this.log.debug(`*ZCFG${zone}BALL${balance}\r`); + } + } + + zoneConfigLoudComp(zone: number, enable: any) + { + this.port.write(`*ZCFG${zone}LOUDCMP${enable}\r`); + this.log.debug(`*ZCFG${zone}LOUDCMP${enable}\r`); + } + //Status functions zoneAskStatus(zone: number) { @@ -178,6 +213,12 @@ export class NuvoSerial this.log.debug(`*ZCFG${zone}STATUS?\r`); } + zoneAskEQ(zone: number) + { + this.port.write(`*ZCFG${zone}EQ?\r`); + this.log.debug(`*ZCFG${zone}EQ?\r`); + } + allZoneConfig() { for (var i = 1; i <= this.numZones; i++) { @@ -231,59 +272,69 @@ export class NuvoSerial { var parts = data.split(",") if (!this.platform.cli) { - if ("#Z" === parts[0].substring(0,2)) - { - if ("#ZCFG" === parts[0].substring(0,5)) - { + if ("#Z" === parts[0].substring(0,2)) { + if ("#ZCFG" === parts[0].substring(0,5)) { let zone = parseInt(parts[0].substring(5)) this.platform.addZone(zone, parts); - } - else - { + } else { let zone = parseInt(parts[0].substring(2)) this.platform.updateZone(zone, parts); } - } - else if ("#SCFG" === parts[0].substring(0,5)) - { + } else if ("#SCFG" === parts[0].substring(0,5)) { let source = parseInt(parts[0].substring(5)) this.platform.addSource(source, parts); } } else { let formatted = {}; if ("#ZCFG" === parts[0].substring(0,5)) { - if (parts[1].substring(6) === "1") - { - formatted['enabled'] = true; - formatted['name'] = parts[2].substring(5, parts[2].length-1); - formatted['linkedTo'] = parts[3].substring(7); - formatted['group'] = parts[4].substring(5); - formatted['sourcesEnabled'] = parseInt(parts[5].substring(7)); + formatted['zone'] = parseInt(parts[0].substring(5)); + if (parts[1].substring(0,6) === "ENABLE") { + if (parts[1].substring(6) === "1") { + formatted['enabled'] = true; + formatted['name'] = parts[2].substring(5, parts[2].length-1); + // formatted['linkedTo'] = parts[3].substring(7); + // formatted['group'] = parts[4].substring(5); + // formatted['sourcesEnabled'] = parseInt(parts[5].substring(7)); + } else { + formatted['enabled'] = false; + } + } else { + formatted['bass'] = parseInt(parts[1].substring(4)); + formatted['treble'] = parseInt(parts[2].substring(4)); + if (parts[3].substring(3, 4) === "C") { + formatted['balance'] = 0; + } else { + formatted['balance'] = parseInt(((parts[3].substring(3, 4) === "R") ? '+' : '-') + parts[3].substring(4)); + } + formatted['loudcomp'] = parts[4].substring(7) === "1"; } - else - { - formatted['enabled'] = false; + + this.log.log('nuvo', formatted); + this.platform.outstandingCmds--; + if (this.platform.outstandingCmds <= 0) { + this.platform.prompt(); + this.platform.outstandingCmds = 0; } } else if ("#SCFG" === parts[0].substring(0,5)) { - formatted['Source'] = parts[0].substring(5); + formatted['source'] = parts[0].substring(5); - if (parts[1].substring(6) === "1") - { + if (parts[1].substring(6) === "1") { formatted['enabled'] = true; formatted['name'] = parts[2].substring(5, parts[2].length-1); - formatted['gain'] = parts[3].substring(4); - formatted['nuvonet'] = parts[4].substring(7) === "1"; + formatted['gain'] = parseInt(parts[3].substring(4)); + // formatted['nuvonet'] = parts[4].substring(7) === "1"; formatted['shortname'] = parts[5].substring(10, 13); - } - else - { + } else { formatted['enabled'] = false; } + this.log.log('nuvo', formatted); + this.platform.outstandingCmds--; + if (this.platform.outstandingCmds <= 0) { + this.platform.prompt(); + this.platform.outstandingCmds = 0; + } } - this.log.log("\n"); - this.log.log(formatted); - this.platform.prompt(); } }); }