UNPKG

heos-api

Version:

🎵 A zero-dependency low level api-wrapper for communicating with HEOS devices 🎵

936 lines (855 loc) • 33.1 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('dgram'), require('net')) : typeof define === 'function' && define.amd ? define(['exports', 'dgram', 'net'], factory) : (factory((global.heosApi = {}),global.dgram,global.net)); }(this, (function (exports,dgram,net) { 'use strict'; var DEFAULT_PORT = 1255; var FIRST_LEVEL_HEOS_RESPONSE_MESSAGE_SPLITTER = '&'; var SECOND_LEVEL_HEOS_RESPONSE_MESSAGE_SPLITTER = '='; var domain; // This constructor is used to store event handlers. Instantiating this is // faster than explicitly calling `Object.create(null)` to get a "clean" empty // object (tested with v8 v4.9). function EventHandlers() {} EventHandlers.prototype = Object.create(null); function EventEmitter() { EventEmitter.init.call(this); } // nodejs oddity // require('events') === require('events').EventEmitter EventEmitter.EventEmitter = EventEmitter; EventEmitter.usingDomains = false; EventEmitter.prototype.domain = undefined; EventEmitter.prototype._events = undefined; EventEmitter.prototype._maxListeners = undefined; // By default EventEmitters will print a warning if more than 10 listeners are // added to it. This is a useful default which helps finding memory leaks. EventEmitter.defaultMaxListeners = 10; EventEmitter.init = function() { this.domain = null; if (EventEmitter.usingDomains) { // if there is an active domain, then attach to it. if (domain.active && !(this instanceof domain.Domain)) ; } if (!this._events || this._events === Object.getPrototypeOf(this)._events) { this._events = new EventHandlers(); this._eventsCount = 0; } this._maxListeners = this._maxListeners || undefined; }; // Obviously not all Emitters should be limited to 10. This function allows // that to be increased. Set to zero for unlimited. EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) { if (typeof n !== 'number' || n < 0 || isNaN(n)) throw new TypeError('"n" argument must be a positive number'); this._maxListeners = n; return this; }; function $getMaxListeners(that) { if (that._maxListeners === undefined) return EventEmitter.defaultMaxListeners; return that._maxListeners; } EventEmitter.prototype.getMaxListeners = function getMaxListeners() { return $getMaxListeners(this); }; // These standalone emit* functions are used to optimize calling of event // handlers for fast cases because emit() itself often has a variable number of // arguments and can be deoptimized because of that. These functions always have // the same number of arguments and thus do not get deoptimized, so the code // inside them can execute faster. function emitNone(handler, isFn, self) { if (isFn) handler.call(self); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self); } } function emitOne(handler, isFn, self, arg1) { if (isFn) handler.call(self, arg1); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1); } } function emitTwo(handler, isFn, self, arg1, arg2) { if (isFn) handler.call(self, arg1, arg2); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2); } } function emitThree(handler, isFn, self, arg1, arg2, arg3) { if (isFn) handler.call(self, arg1, arg2, arg3); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].call(self, arg1, arg2, arg3); } } function emitMany(handler, isFn, self, args) { if (isFn) handler.apply(self, args); else { var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].apply(self, args); } } EventEmitter.prototype.emit = function emit(type) { var er, handler, len, args, i, events, domain; var doError = (type === 'error'); events = this._events; if (events) doError = (doError && events.error == null); else if (!doError) return false; domain = this.domain; // If there is no 'error' event listener then throw. if (doError) { er = arguments[1]; if (domain) { if (!er) er = new Error('Uncaught, unspecified "error" event'); er.domainEmitter = this; er.domain = domain; er.domainThrown = false; domain.emit('error', er); } else if (er instanceof Error) { throw er; // Unhandled 'error' event } else { // At least give some kind of context to the user var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); err.context = er; throw err; } return false; } handler = events[type]; if (!handler) return false; var isFn = typeof handler === 'function'; len = arguments.length; switch (len) { // fast cases case 1: emitNone(handler, isFn, this); break; case 2: emitOne(handler, isFn, this, arguments[1]); break; case 3: emitTwo(handler, isFn, this, arguments[1], arguments[2]); break; case 4: emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); break; // slower default: args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; emitMany(handler, isFn, this, args); } return true; }; function _addListener(target, type, listener, prepend) { var m; var events; var existing; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = target._events; if (!events) { events = target._events = new EventHandlers(); target._eventsCount = 0; } else { // To avoid recursion in the case that type === "newListener"! Before // adding it to the listeners, first emit "newListener". if (events.newListener) { target.emit('newListener', type, listener.listener ? listener.listener : listener); // Re-assign `events` because a newListener handler could have caused the // this._events to be assigned to a new object events = target._events; } existing = events[type]; } if (!existing) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else { // If we've already got an array, just append. if (prepend) { existing.unshift(listener); } else { existing.push(listener); } } // Check for listener leak if (!existing.warned) { m = $getMaxListeners(target); if (m && m > 0 && existing.length > m) { existing.warned = true; var w = new Error('Possible EventEmitter memory leak detected. ' + existing.length + ' ' + type + ' listeners added. ' + 'Use emitter.setMaxListeners() to increase limit'); w.name = 'MaxListenersExceededWarning'; w.emitter = target; w.type = type; w.count = existing.length; emitWarning(w); } } } return target; } function emitWarning(e) { typeof console.warn === 'function' ? console.warn(e) : console.log(e); } EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener; EventEmitter.prototype.prependListener = function prependListener(type, listener) { return _addListener(this, type, listener, true); }; function _onceWrap(target, type, listener) { var fired = false; function g() { target.removeListener(type, g); if (!fired) { fired = true; listener.apply(target, arguments); } } g.listener = listener; return g; } EventEmitter.prototype.once = function once(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.on(type, _onceWrap(this, type, listener)); return this; }; EventEmitter.prototype.prependOnceListener = function prependOnceListener(type, listener) { if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); this.prependListener(type, _onceWrap(this, type, listener)); return this; }; // emits a 'removeListener' event iff the listener was removed EventEmitter.prototype.removeListener = function removeListener(type, listener) { var list, events, position, i, originalListener; if (typeof listener !== 'function') throw new TypeError('"listener" argument must be a function'); events = this._events; if (!events) return this; list = events[type]; if (!list) return this; if (list === listener || (list.listener && list.listener === listener)) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else { delete events[type]; if (events.removeListener) this.emit('removeListener', type, list.listener || listener); } } else if (typeof list !== 'function') { position = -1; for (i = list.length; i-- > 0;) { if (list[i] === listener || (list[i].listener && list[i].listener === listener)) { originalListener = list[i].listener; position = i; break; } } if (position < 0) return this; if (list.length === 1) { list[0] = undefined; if (--this._eventsCount === 0) { this._events = new EventHandlers(); return this; } else { delete events[type]; } } else { spliceOne(list, position); } if (events.removeListener) this.emit('removeListener', type, originalListener || listener); } return this; }; EventEmitter.prototype.removeAllListeners = function removeAllListeners(type) { var listeners, events; events = this._events; if (!events) return this; // not listening for removeListener, no need to emit if (!events.removeListener) { if (arguments.length === 0) { this._events = new EventHandlers(); this._eventsCount = 0; } else if (events[type]) { if (--this._eventsCount === 0) this._events = new EventHandlers(); else delete events[type]; } return this; } // emit removeListener for all listeners on all events if (arguments.length === 0) { var keys = Object.keys(events); for (var i = 0, key; i < keys.length; ++i) { key = keys[i]; if (key === 'removeListener') continue; this.removeAllListeners(key); } this.removeAllListeners('removeListener'); this._events = new EventHandlers(); this._eventsCount = 0; return this; } listeners = events[type]; if (typeof listeners === 'function') { this.removeListener(type, listeners); } else if (listeners) { // LIFO order do { this.removeListener(type, listeners[listeners.length - 1]); } while (listeners[0]); } return this; }; EventEmitter.prototype.listeners = function listeners(type) { var evlistener; var ret; var events = this._events; if (!events) ret = []; else { evlistener = events[type]; if (!evlistener) ret = []; else if (typeof evlistener === 'function') ret = [evlistener.listener || evlistener]; else ret = unwrapListeners(evlistener); } return ret; }; EventEmitter.listenerCount = function(emitter, type) { if (typeof emitter.listenerCount === 'function') { return emitter.listenerCount(type); } else { return listenerCount.call(emitter, type); } }; EventEmitter.prototype.listenerCount = listenerCount; function listenerCount(type) { var events = this._events; if (events) { var evlistener = events[type]; if (typeof evlistener === 'function') { return 1; } else if (evlistener) { return evlistener.length; } } return 0; } EventEmitter.prototype.eventNames = function eventNames() { return this._eventsCount > 0 ? Reflect.ownKeys(this._events) : []; }; // About 1.5x faster than the two-arg version of Array#splice(). function spliceOne(list, index) { for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) list[i] = list[k]; list.pop(); } function arrayClone(arr, i) { var copy = new Array(i); while (i--) copy[i] = arr[i]; return copy; } function unwrapListeners(arr) { var ret = new Array(arr.length); for (var i = 0; i < ret.length; ++i) { ret[i] = arr[i].listener || arr[i]; } return ret; } function generateHeosCommandString(command) { return command.commandGroup + '/' + command.command; } function parseHeosCommandString(commandString) { var _a = commandString.split('/'), commandGroup = _a[0], command = _a[1]; return { commandGroup: commandGroup, command: command }; } var ResponseEventHandler = /** @class */ (function () { function ResponseEventHandler() { this.emitter = new EventEmitter(); this.listenersOnAll = []; } ResponseEventHandler.prototype.put = function (message) { var eventString = generateHeosCommandString(message.heos.command); this.listenersOnAll.forEach(function (listener) { return listener(message); }); this.emitter.emit(eventString, message); }; ResponseEventHandler.prototype.on = function (event, listener) { var eventString = generateHeosCommandString(event); this.emitter.on(eventString, listener); return this; }; ResponseEventHandler.prototype.once = function (event, listener) { var eventString = generateHeosCommandString(event); this.emitter.once(eventString, listener); return this; }; ResponseEventHandler.prototype.onAll = function (listener) { this.listenersOnAll = this.listenersOnAll.concat([listener]); return this; }; return ResponseEventHandler; }()); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ var __assign = function() { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function parseHeosMessageString(messageString) { var parsedMessageObject = {}; if (typeof messageString === 'string') { parsedMessageObject = messageString .split(FIRST_LEVEL_HEOS_RESPONSE_MESSAGE_SPLITTER) .reduce(function (parsedPart, keyAndValuePair) { var _a; var _b = keyAndValuePair.split(SECOND_LEVEL_HEOS_RESPONSE_MESSAGE_SPLITTER), key = _b[0], value = _b[1], rest = _b.slice(2); return __assign({}, parsedPart, (key && value && !rest.length && (_a = {}, _a[key] = !isNaN(Number(value)) ? Number(value) : value, _a))); }, parsedMessageObject); } return __assign({}, (typeof messageString === 'string' && { unparsed: messageString }), (Object.keys(parsedMessageObject).length && { parsed: parsedMessageObject })); } var messageDelimiter = '\r\n'; function isValidHeosResponseMessage(message) { if (!Object.keys(message).length || (message.hasOwnProperty('unparsed') && typeof message.unparsed === 'string' && (!message.hasOwnProperty('parsed') || (message.hasOwnProperty('parsed') && typeof message.parsed === 'object')))) { return true; } return false; } function isCorrectResponse(response) { return (response.hasOwnProperty('heos') && response.heos.hasOwnProperty('command') && typeof response.heos.command === 'string'); } function isHeosResponse(response) { if (response.hasOwnProperty('heos')) { var heos = response.heos; if (heos.hasOwnProperty('command') && heos.hasOwnProperty('result') && heos.hasOwnProperty('message')) { if (typeof heos.command === 'object' && typeof heos.result === 'string' && typeof heos.message === 'object' && isValidHeosResponseMessage(heos.message)) { if (heos.command.hasOwnProperty('commandGroup') && heos.command.hasOwnProperty('command')) { if (typeof heos.command.commandGroup === 'string' && typeof heos.command.command === 'string') { return true; } } } } } return false; } function isHeosEvent(response) { if (response.hasOwnProperty('heos')) { var heos = response.heos; if (heos.hasOwnProperty('command')) { if (typeof heos.command === 'object' && heos.command.hasOwnProperty('commandGroup') && heos.command.hasOwnProperty('command') && typeof heos.command.commandGroup === 'string' && typeof heos.command.command === 'string') { if (heos.hasOwnProperty('message') && typeof heos.message === 'object') { return isValidHeosResponseMessage(heos.message); } else { return true; } } } } return false; } var ResponseParser = /** @class */ (function () { function ResponseParser(callback) { this.buffer = ''; this.callback = callback; } ResponseParser.prototype.put = function (data) { var _this = this; this.buffer += data; var messages = this.buffer.split(messageDelimiter); var lastMessage = messages.pop(); if (lastMessage === '') { messages.push(lastMessage); this.buffer = ''; } else { this.buffer = lastMessage || ''; } try { messages .filter(function (row) { return row.length > 0; }) .map(function (message) { return JSON.parse(message); }) .map(function (response) { if (isCorrectResponse) { return response; } else { throw new TypeError(); } }) .map(function (response) { var command = parseHeosCommandString(response.heos.command); response.heos.command = command; return response; }) .map(function (response) { var message = parseHeosMessageString(response.heos.message); response.heos.message = message; return response; }) .map(function (response) { if (isHeosResponse(response) || isHeosEvent(response)) { return response; } else { throw new TypeError(); } }) .forEach(function (response) { try { _this.callback(response); } catch (error) { console.log('Error handling response'); } }); } catch (error) { if (error instanceof TypeError) { console.log('Heos response has wrong structure. Flushing buffer.'); } else { console.log('Error parsing incoming messages. Flushing buffer.'); } this.buffer = ''; } }; return ResponseParser; }()); var prefix = 'heos://'; var postfix = '\r\n'; function attributeString(attributes) { if (!attributes || Object.entries(attributes).length < 1) { return ''; } else { return ('?' + Object.entries(attributes) .map(function (_a) { var key = _a[0], value = _a[1]; return key + "=" + value; }) .reduce(function (previous, current) { return previous + "&" + current; })); } } function generateHeosCommand(commandGroup, command, attributes) { if (!commandGroup || !command) { throw new Error('Missing arguments when creating HeosCommand'); } return [prefix, commandGroup, '/', command, attributeString(attributes), postfix].join(''); } /** * An object representing a connection with a HEOS device, and provides methods to communicate with the connected HEOS device. * @remark All the methods returns a HeosConnection which means that they are chainable. */ var HeosConnection = /** @class */ (function () { function HeosConnection(on, once, onAll, socket) { var _this = this; this.closed = false; this.on = function (event, listener) { if (_this.closed) { console.warn('You are trying to add an event listener to a closed HeosConnection.'); } else { on(event, listener); } return _this; }; this.once = function (event, listener) { if (_this.closed) { console.warn('You are trying to add an event listener to a closed HeosConnection.'); } else { once(event, listener); } return _this; }; this.onAll = function (listener) { if (_this.closed) { console.warn('You are trying to add an event listener to a closed HeosConnection.'); } else { onAll(listener); } return _this; }; this.socket = socket; } /** * Sends a command to the connected HEOS device. Check the [HEOS CLI Protocol Specification](http://rn.dmglobal.com/euheos/HEOS_CLI_ProtocolSpecification.pdf) to learn all the commands that can be sent. * @param commandGroup The command group * @param command The command to send * @param attributes Optional attributes to include with the command * @returns A HeosConnection */ HeosConnection.prototype.write = function (commandGroup, command, attributes) { if (this.closed) { console.warn('You are trying to write to a closed HeosConnection.'); } else { this.socket.write(generateHeosCommand(commandGroup, command, attributes)); } return this; }; /** * Closes the HeosConnection. It is still possible that the connected HEOS Device will send messages after calling this command. * @returns A promise that resolves when the connection is finished. No messages will be sent from the HEOS Device after the promise is resolved. */ HeosConnection.prototype.close = function () { var _this = this; this.closed = true; return new Promise(function (resolve) { _this.socket.end('', undefined, resolve); }); }; /** * Adds an event listener for when the connection is closed * @param listener A callback that is called when the connection is closed. `hadError` is true if there was a transmission error. */ HeosConnection.prototype.onClose = function (listener) { this.socket.on('close', listener); return this; }; /** * Adds an event listener for when an error occurs. * @param listener A callback thar is called when an error occurs. */ HeosConnection.prototype.onError = function (listener) { this.socket.on('error', listener); return this; }; return HeosConnection; }()); function createHeosSocket(address, responseParser) { var hasResolvedOrRejected = false; return new Promise(function (resolve, reject) { var host = address; var port = DEFAULT_PORT; try { var socket_1 = net.createConnection({ port: port, host: host, localPort: 0 }, function () { hasResolvedOrRejected = true; resolve(socket_1); }); socket_1.on('data', function (data) { return responseParser.put(data); }); socket_1.on('timeout', function () { if (!hasResolvedOrRejected) { socket_1.end(); hasResolvedOrRejected = true; reject(new Error('Socket timeout')); } }); socket_1.on('error', function (error) { if (!hasResolvedOrRejected) { hasResolvedOrRejected = true; reject(error); } else { console.error(error); } }); } catch (error) { reject(error); } }); } /** * Establishes a connection with a HEOS device. Use this function when you know the address of a HEOS device. It is recommended to use `hoes.discoverOneDevice()` to find an address. * @param address The address of the host (HEOS device) to create a connection to. * @returns A promise that resolves with a HeosConnection that can be used to communicate with a HEOS device. */ function connect(address) { return new Promise(function (resolve, reject) { var responseEventHandler = new ResponseEventHandler(); var responseParser = new ResponseParser(function (message) { return responseEventHandler.put(message); }); createHeosSocket(address, responseParser) .then(function (socket) { var on = function (event, listener) { return responseEventHandler.on(event, listener); }; var once = function (event, listener) { return responseEventHandler.once(event, listener); }; var onAll = function (listener) { return responseEventHandler.onAll(listener); }; var connection = new HeosConnection(on, once, onAll, socket); resolve(connection); }) .catch(reject); }); } var searchTargetName = 'urn:schemas-denon-com:device:ACT-Denon:1'; var message = [ 'M-SEARCH * HTTP/1.1', 'HOST: 239.255.255.250:1900', "ST: " + searchTargetName, 'MX: 5', 'MAN: "ssdp:discover"', '\r\n' ].join('\r\n'); var defaultTimeout = 5000; /** * Tries to discover all available HEOS devices in the network. * @param options Options for discovering devices. * @param onDiscover Will trigger every time a HEOS device is discovered. * @param onTimeout Will trigger when `timeout` has ellapsed. */ function discoverDevices(options, onDiscover, onTimeout) { var timeout = typeof options === 'number' ? options || defaultTimeout : options.timeout || defaultTimeout; var socket = dgram.createSocket('udp4'); typeof options !== 'number' ? socket.bind(options.port, options.address) : socket.bind(); socket.on('listening', function () { socket.send(message, 1900, '239.255.255.250'); }); var addresses = []; socket.on('message', function (msg, rinfo) { if (msg.includes(searchTargetName)) { onDiscover(rinfo.address); addresses.push(rinfo.address); } }); var timeOutReferance = setTimeout(quit, timeout); function quit() { socket.close(); if (onTimeout) { onTimeout(addresses); } global.clearTimeout(timeOutReferance); } return quit; } /** * Finds one HEOS device in the network. * @param options Options for discovering a device. * @returns A promise that will resolve when the first device is found, or reject if no devices are found before `timeout` milliseconds have passed. If the function resolves it will resolve with the address of the HEOS device found. */ function discoverOneDevice(options) { if (options === void 0) { options = defaultTimeout; } return new Promise(function (resolve, reject) { var oneDiscovered = false; function onDiscover(adress) { if (oneDiscovered) { return; } oneDiscovered = true; quit(); resolve(adress); } function onTimeout(adresses) { if (!oneDiscovered) { reject('No devices found'); } } var quit = discoverDevices(options, onDiscover, onTimeout); }); } /** * Finds one HEOS device in the network, and connects to it. * @param options Options for discovering a device. * @returns A promise that will resolve when the first device is found, or reject if no devices are found before `timeout` milliseconds have passed. If the function resolves it will resolve with a HeosConnection. */ function discoverAndConnect(options) { if (options === void 0) { options = defaultTimeout; } return new Promise(function (resolve, reject) { discoverOneDevice(options) .then(connect) .then(resolve) .catch(reject); }); } exports.discoverDevices = discoverDevices; exports.discoverOneDevice = discoverOneDevice; exports.discoverAndConnect = discoverAndConnect; exports.connect = connect; Object.defineProperty(exports, '__esModule', { value: true }); }))); //# sourceMappingURL=heos-api.umd.js.map