UNPKG

homebridge-denon-heos-audio

Version:

Control your Denon speakers via AVR Control and/or HeosCLI

293 lines 11.4 kB
import { Mutex } from "async-mutex"; import * as net from "net"; export class DenonClient { serialNumber; params; socket; sendMutex; dataEventMutex; pendingData; responseCallback; debugLogCallback; powerUpdateCallback; muteUpdateCallback; volumeUpdateCallback; constructor(serialNumber, params, debugLogCallback, powerUpdateCallback, muteUpdateCallback, volumeUpdateCallback) { this.serialNumber = serialNumber; this.params = params; this.socket = undefined; this.sendMutex = new Mutex(); this.dataEventMutex = new Mutex(); this.pendingData = ""; this.responseCallback = undefined; this.debugLogCallback = debugLogCallback; this.powerUpdateCallback = powerUpdateCallback; this.muteUpdateCallback = muteUpdateCallback; this.volumeUpdateCallback = volumeUpdateCallback; } async connect() { const release = await this.sendMutex.acquire(); try { if (!this.isConnected()) { await this.connectUnchecked(); } return; } finally { release(); } } async connectUnchecked() { // Connect let newSocket = undefined; try { this.debugLog("Establishing new connection..."); this.pendingData = ""; this.responseCallback = undefined; newSocket = await new Promise((resolve, reject) => { newSocket = net.createConnection({ host: this.params.host, port: this.params.port, timeout: 0, // no timeout }, () => { this.debugLog("New connection established."); resolve(newSocket); }); // Listen for connection closure events newSocket.on("timeout", () => { // TODO - right now we do not have client-side timeouts this.socket = undefined; }); newSocket.on("close", () => { this.debugLog("Connection has closed."); newSocket?.destroy(); this.socket = undefined; }); newSocket.on("error", (error) => { this.debugLog("Received an error from the server.", error); newSocket?.end(); setTimeout(() => { // give the server 2 seconds to gracefully close the connection, then force the issue if (!newSocket?.destroyed) { this.debugLog("Connection destroyed."); newSocket?.destroy(); } }, 2000); this.socket = undefined; reject(error); }); // Listen for responses newSocket.on("data", (data) => { this.dataHandler(data); }); setTimeout(() => { reject(new ConnectionTimeoutException(this.params)); }, this.params.connect_timeout); }); this.socket = newSocket; await this.subscribeToChangeEvents(); } catch (error) { if (newSocket) { this.debugLog("Destroying socket."); newSocket.destroy(); // Destroy the connection attempt } this.debugLog("Catching and throwing:", error); throw error; } } async sendCommand(command, commandMode, { pid, value, passPayload = false }) { const specCommand = command[CommandMode[commandMode]]; let commandStr = specCommand.COMMAND; if (specCommand.PARAMS) { commandStr += specCommand.PARAMS; } if (pid) { commandStr = commandStr.replace("[PID]", pid); } if (value) { commandStr = commandStr.replace("[VALUE]", value); } if (specCommand.EXP_RES) { const response = await this.send(commandStr, specCommand.COMMAND, specCommand.EXP_RES, passPayload); if (command.VALUES) { const mappedValue = findMapByValue(command.VALUES, response); if (mappedValue === undefined) { throw new InvalidResponseException(`Unexpected response for command ${commandStr}`, Object.values(command.VALUES).map((value) => value.VALUE), response); } } return response; } else { return await this.send(commandStr, specCommand.COMMAND, undefined, passPayload); } } async send(command, rawCommand, expectedResponse, passPayload = false) { const release = await this.sendMutex.acquire(); try { if (!this.isConnected()) { await this.connectUnchecked(); } return await this.sendUnchecked(command, rawCommand, expectedResponse, passPayload); } finally { release(); } } async sendUnchecked(command, rawCommand, expectedResponse, passPayload = false) { const fullCommand = this.params.command_prefix ? this.params.command_prefix + command : command; this.debugLog("Sending command:", fullCommand); if (expectedResponse) { return new Promise((resolve, reject) => { this.responseCallback = new ResponseCallback((response) => { resolve(response); }, rawCommand ?? command, expectedResponse, passPayload); this.socket.write(fullCommand + this.params.command_separator); setTimeout(() => { reject(new ResponseTimeoutException(fullCommand, this.params.response_timeout)); }, this.params.response_timeout); }).finally(() => { this.responseCallback = undefined; }); } else { return new Promise((resolve) => { this.socket.write(fullCommand + this.params.command_separator); resolve(""); }); } } async dataHandler(incomingData) { const release = await this.dataEventMutex.acquire(); let chunks; try { const currentData = this.pendingData ? this.pendingData + incomingData.toString() : incomingData.toString(); chunks = currentData.split(this.params.response_separator); this.pendingData = chunks[chunks.length - 1]; } finally { release(); } for (let i = 0; i <= chunks.length - 2; i++) { this.responseRouter(chunks[i]); } } debugLog(message, ...parameters) { if (this.debugLogCallback) { this.debugLogCallback(message, ...parameters); } } isConnected() { return this.socket !== undefined; } } export var CommandMode; (function (CommandMode) { CommandMode[CommandMode["GET"] = 0] = "GET"; CommandMode[CommandMode["SET"] = 1] = "SET"; })(CommandMode || (CommandMode = {})); class ResponseCallback { callback; command; expectedResponse; passPayload; constructor(callback, command, expectedResponse, passPayload = false) { this.callback = callback; this.command = command; this.expectedResponse = expectedResponse; this.passPayload = passPayload; } } export class RaceStatus { static ID_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; running = true; raceId = RaceStatus.ID_CHARS[Math.floor(Math.random() * 36)] + RaceStatus.ID_CHARS[Math.floor(Math.random() * 36)]; isRunning() { return this.running; } setRaceOver() { this.running = false; } } export var Playing; (function (Playing) { Playing[Playing["PLAY"] = 0] = "PLAY"; Playing[Playing["PAUSE"] = 1] = "PAUSE"; Playing[Playing["STOP"] = 2] = "STOP"; Playing[Playing["UNSUPPORTED"] = 3] = "UNSUPPORTED"; })(Playing || (Playing = {})); export const isPlaying = { [Playing.PLAY]: true, [Playing.PAUSE]: false, [Playing.STOP]: false, [Playing.UNSUPPORTED]: false, }; export function findMapByValue(values, mapValue) { for (const key in values) { if (values[key].VALUE === mapValue) { return values[key].MAP; } } return undefined; } export function findValueByMap(values, mapValue) { for (const key in values) { if (values[key].MAP === mapValue) { return values[key].VALUE; } } return undefined; } export class ConnectionTimeoutException extends Error { constructor(params) { super(`connection to ${params.host}:${params.port} timed out after ${params.connect_timeout}ms.`); // Pass the message to the parent Error class this.name = "ConnectionTimeoutException"; // Set the error name // Ensure the prototype chain is correctly set for instanceof checks Object.setPrototypeOf(this, ConnectionTimeoutException.prototype); } } export class ResponseTimeoutException extends Error { constructor(command, timeout) { super(`response for command '${command}' timed out after ${timeout}ms.`); // Pass the message to the parent Error class this.name = "ResponseTimeoutException"; // Set the error name // Ensure the prototype chain is correctly set for instanceof checks Object.setPrototypeOf(this, ResponseTimeoutException.prototype); } } export class CommandFailedException extends Error { constructor(command) { super(`Execution of command "${command}" has failed on the server side.`); // Pass the message to the parent Error class this.name = "CommandFailedException"; // Set the error name // Ensure the prototype chain is correctly set for instanceof checks Object.setPrototypeOf(this, CommandFailedException.prototype); } } export class InvalidResponseException extends Error { expectedResponses; actualResponse; constructor(message, expectedResponses, actualResponse) { super(InvalidResponseException.buildFullMessage(message, expectedResponses, actualResponse)); // Pass the message to the parent Error class this.name = "InvalidResponseException"; // Set the error name this.expectedResponses = expectedResponses; this.actualResponse = actualResponse; // Ensure the prototype chain is correctly set for instanceof checks Object.setPrototypeOf(this, InvalidResponseException.prototype); } static buildFullMessage(message, expectedResponses, actualResponse) { let fullMessage = message; //let fullMessage = message; if (expectedResponses) { if (actualResponse) { fullMessage += ` (Actual response: ${JSON.stringify(actualResponse)}; Expected responses: ${expectedResponses.join(" | ")})`; } else { fullMessage += ` (Expected response: ${expectedResponses.join(" | ")})`; } } else if (actualResponse) { fullMessage += ` (Actual response: ${actualResponse})`; } return fullMessage; } } //# sourceMappingURL=denonClient.js.map