UNPKG

homebridge-denon-heos-audio

Version:

Control your Denon speakers via AVR Control and/or HeosCLI

346 lines 14.4 kB
import { CommandFailedException, CommandMode, DenonClient, findMapByValue, findValueByMap, InvalidResponseException, isPlaying, Playing, } from "./denonClient.js"; import * as DenonProtocol from "./denonProtocol.js"; export class DenonClientHeosCli extends DenonClient { controlMode = DenonProtocol.ControlMode.HEOSCLI; static PROTOCOL = { GLOBAL_CHANGE_EVENT_REGEX: /^event\/(\w+)$/, PID_REGEX: /pid=(\d+)/, EVENT_SUB: { SET: { COMMAND: "system/register_for_change_events", PARAMS: "?enable=[VALUE]", EXP_RES: /enable=(\w+)/, }, VALUES: { ON: { VALUE: "on", MAP: true, }, OFF: { VALUE: "off", MAP: false, }, }, }, PLAYERS: { GET: { COMMAND: "player/get_players", PARAMS: "", EXP_RES: /^$/, }, }, PLAY_STATE: { GET: { COMMAND: "player/get_play_state", PARAMS: "?pid=[PID]", EXP_RES: /state=(\w+)/, }, SET: { COMMAND: "player/set_play_state", PARAMS: "?pid=[PID]&state=[VALUE]", EXP_RES: /state=(\w+)/, }, EVENT: { COMMAND: "event/player_state_changed", EXP_RES: /state=(\w+)/, }, VALUES: { PLAY: { VALUE: "play", MAP: Playing.PLAY, }, PAUSE: { VALUE: "pause", MAP: Playing.PAUSE, }, STOP: { VALUE: "stop", MAP: Playing.STOP, }, }, }, PLAY_NEXT: { SET: { COMMAND: "player/play_next", PARAMS: "?pid=[PID]", EXP_RES: /.*/, }, }, PLAY_PREVIOUS: { SET: { COMMAND: "player/play_previous", PARAMS: "?pid=[PID]", EXP_RES: /.*/, }, }, MUTE: { GET: { COMMAND: "player/get_mute", PARAMS: "?pid=[PID]", EXP_RES: /state=(\w+)/, }, SET: { COMMAND: "player/set_mute", PARAMS: "?pid=[PID]&state=[VALUE]", EXP_RES: /state=(\w+)/, }, EVENT: { EVENT: "event/player_volume_changed", EXP_RES: /mute=(\w+)/, }, VALUES: { ON: { VALUE: "on", MAP: true, }, OFF: { VALUE: "off", MAP: false, }, }, }, VOLUME: { GET: { COMMAND: "player/get_volume", PARAMS: "?pid=[PID]", EXP_RES: /level=(\d+)/, }, SET: { COMMAND: "player/set_volume", PARAMS: "?pid=[PID]&level=[VALUE]", EXP_RES: /level=(\d+)/, }, EVENT: { EVENT: "event/player_volume_changed", EXP_RES: /level=(\d+)/, }, }, VOLUME_UP: { SET: { COMMAND: "player/volume_up", PARAMS: "?pid=[PID]&step=[VALUE]", EXP_RES: /step=(\d+)/, }, }, VOLUME_DOWN: { SET: { COMMAND: "player/volume_down", PARAMS: "?pid=[PID]&step=[VALUE]", EXP_RES: /step=(\d+)/, }, }, }; player_id; constructor(serialNumber, host, connect_timeout, response_timeout, debugLogCallback, powerUpdateCallback, muteUpdateCallback, volumeUpdateCallback) { super(serialNumber, { host: host, port: DenonProtocol.ControlMode.HEOSCLI, connect_timeout: connect_timeout, response_timeout: response_timeout, command_prefix: "heos://", command_separator: "\r\n", response_separator: "\r\n", all_responses_to_generic: false, }, debugLogCallback, powerUpdateCallback, muteUpdateCallback, volumeUpdateCallback); this.player_id = undefined; this.findPlayerId(); } async findPlayerId() { const payload_str = await super.sendCommand(DenonClientHeosCli.PROTOCOL.PLAYERS, CommandMode.GET, { passPayload: true }); const payload = JSON.parse(payload_str); if (!Array.isArray(payload)) { throw new InvalidResponseException("Payload is not an array!", undefined, payload_str); } for (const player of payload) { if (player.serial === this.serialNumber) { this.player_id = player.pid; return; } } throw new InvalidResponseException("Player list does not include serial " + this.serialNumber); } async subscribeToChangeEvents() { const commandStr = (DenonClientHeosCli.PROTOCOL.EVENT_SUB.SET.COMMAND + DenonClientHeosCli.PROTOCOL.EVENT_SUB.SET.PARAMS).replace("[VALUE]", DenonClientHeosCli.PROTOCOL.EVENT_SUB.VALUES.ON.VALUE); const response = await this.sendUnchecked(commandStr, DenonClientHeosCli.PROTOCOL.EVENT_SUB.SET.COMMAND, DenonClientHeosCli.PROTOCOL.EVENT_SUB.SET.EXP_RES); const mappedValue = findMapByValue(DenonClientHeosCli.PROTOCOL.EVENT_SUB.VALUES, response); if (mappedValue === undefined) { throw new InvalidResponseException("No valid response!", Object.values(DenonClientHeosCli.PROTOCOL.EVENT_SUB.VALUES).map((value) => value.VALUE), response); } if (response !== DenonClientHeosCli.PROTOCOL.EVENT_SUB.VALUES.ON.VALUE) { throw new CommandFailedException(commandStr); } } async sendCommand(command, commandMode, { value, passPayload = false }) { if (!this.player_id) { this.findPlayerId(); } return super.sendCommand(command, commandMode, { pid: this.player_id, value: value, passPayload: passPayload }); } responseRouter(response) { let r_obj; try { r_obj = JSON.parse(response); } catch { throw new InvalidResponseException("Received a response that is not valid JSON!", undefined, response); } if (r_obj.heos === undefined || r_obj.heos.message === undefined) { throw new InvalidResponseException("Received response that does not follow HeosCLI specifications", undefined, response); } if (this.responseCallback && this.responseCallback.command === r_obj.heos.command.trim()) { let out = undefined; if (r_obj.heos.success !== undefined && r_obj.heos.success !== "success") { const error = new CommandFailedException(this.responseCallback.command); this.responseCallback = undefined; throw error; } const pid_match = r_obj.heos.message.match(DenonClientHeosCli.PROTOCOL.PID_REGEX); if (!pid_match || Number(pid_match[1]) === this.player_id) { const message_match = r_obj.heos.message.match(this.responseCallback.expectedResponse); if (message_match) { out = message_match.length > 1 ? message_match[1] : r_obj.heos.message; } } if (out !== undefined) { if (this.responseCallback.passPayload) { if (r_obj.payload === undefined) { throw new InvalidResponseException("Received a response does not include a payload!"); } out = JSON.stringify(r_obj.payload); } this.debugLog("Received response:", response); this.responseCallback.callback(out); this.responseCallback = undefined; if (this.params.all_responses_to_generic) { this.genericResponseHandler(r_obj); } } else { this.genericResponseHandler(r_obj); } } else { this.genericResponseHandler(r_obj); } } genericResponseHandler(r_obj) { if (!r_obj.heos.command.match(DenonClientHeosCli.PROTOCOL.GLOBAL_CHANGE_EVENT_REGEX)) { return; // not an event } const pid_match = r_obj.heos.message.match(DenonClientHeosCli.PROTOCOL.PID_REGEX); if (!pid_match || Number(pid_match[1]) !== this.player_id) { return; // not this player } this.debugLog("Received change event:", JSON.stringify(r_obj)); // Play state if (r_obj.heos.command === DenonClientHeosCli.PROTOCOL.PLAY_STATE.EVENT.COMMAND) { const match = r_obj.heos.message.match(DenonClientHeosCli.PROTOCOL.PLAY_STATE.EVENT.EXP_RES); if (!match) { throw new InvalidResponseException("Unexpected play state", Object.values(DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES).map((value) => value.VALUE), r_obj.heos.message); } const mappedValue = findMapByValue(DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES, match[1]); if (mappedValue === undefined) { throw new InvalidResponseException("Unexpected play state", Object.values(DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES).map((value) => value.VALUE), r_obj.heos.message); } if (this.powerUpdateCallback) { this.powerUpdateCallback(isPlaying[mappedValue]); } } } async getPlayingInternal() { const response = await this.sendCommand(DenonClientHeosCli.PROTOCOL.PLAY_STATE, CommandMode.GET, {}); const mappedValue = findMapByValue(DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES, response); return mappedValue; } async getPlaying(raceStatus) { const playing = await this.getPlayingInternal(); if (raceStatus && !raceStatus.isRunning()) { this.debugLog(`getPlaying was late to the party [race id: ${raceStatus.raceId}].`); } return playing; } async setPlaying(playing) { const response = await this.sendCommand(DenonClientHeosCli.PROTOCOL.PLAY_STATE, CommandMode.SET, { value: findValueByMap(DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES, playing), }); const mappedValue = findMapByValue(DenonClientHeosCli.PROTOCOL.PLAY_STATE.VALUES, response); return mappedValue; } async setPlayNext() { await this.sendCommand(DenonClientHeosCli.PROTOCOL.PLAY_NEXT, CommandMode.SET, {}); } async setPlayPrevious() { await this.sendCommand(DenonClientHeosCli.PROTOCOL.PLAY_PREVIOUS, CommandMode.SET, {}); } async getPower(raceStatus) { const playing = await this.getPlayingInternal(); if (raceStatus && !raceStatus.isRunning() && this.powerUpdateCallback) { this.powerUpdateCallback(isPlaying[playing]); this.debugLog(`getPower was late to the party [race id: ${raceStatus.raceId}].`); } return isPlaying[playing]; } async setPower(power) { const newPlaying = await this.setPlaying(power ? Playing.PLAY : Playing.STOP); if (this.powerUpdateCallback) { this.powerUpdateCallback(isPlaying[newPlaying]); } return isPlaying[newPlaying]; } async getMute(raceStatus) { const response = await this.sendCommand(DenonClientHeosCli.PROTOCOL.MUTE, CommandMode.GET, {}); const mappedValue = findMapByValue(DenonClientHeosCli.PROTOCOL.MUTE.VALUES, response); if (raceStatus && !raceStatus.isRunning() && this.muteUpdateCallback) { this.muteUpdateCallback(mappedValue); this.debugLog(`getMute was late to the party [race id: ${raceStatus.raceId}].`); } return mappedValue; } async setMute(mute) { const response = await this.sendCommand(DenonClientHeosCli.PROTOCOL.MUTE, CommandMode.SET, { value: findValueByMap(DenonClientHeosCli.PROTOCOL.MUTE.VALUES, mute), }); const mappedValue = findMapByValue(DenonClientHeosCli.PROTOCOL.MUTE.VALUES, response); if (this.muteUpdateCallback) { this.muteUpdateCallback(mappedValue); } return mappedValue; } async getVolume(raceStatus) { const response = await this.sendCommand(DenonClientHeosCli.PROTOCOL.VOLUME, CommandMode.GET, {}); if (raceStatus && !raceStatus.isRunning() && this.volumeUpdateCallback) { this.volumeUpdateCallback(Number(response)); this.debugLog(`getVolume was late to the party [race id: ${raceStatus.raceId}].`); } return Number(response); } async setVolume(volume) { if (volume < 0 || volume > 100) { throw new Error("Volume must be between 0 and 100"); } const response = await this.sendCommand(DenonClientHeosCli.PROTOCOL.VOLUME, CommandMode.SET, { value: Math.round(volume).toString(), }); if (this.volumeUpdateCallback) { this.volumeUpdateCallback(Number(response)); } return Number(response); } async setVolumeUp(volumeIncrement) { if (volumeIncrement < 1 || volumeIncrement > 10) { throw new Error("Volume increment must be between 1 and 10"); } await this.sendCommand(DenonClientHeosCli.PROTOCOL.VOLUME_UP, CommandMode.SET, { value: Math.round(volumeIncrement).toString(), }); } async setVolumeDown(volumeDecrement) { if (volumeDecrement < 1 || volumeDecrement > 10) { throw new Error("Volume decrement must be between 1 and 10"); } await this.sendCommand(DenonClientHeosCli.PROTOCOL.VOLUME_DOWN, CommandMode.SET, { value: Math.round(volumeDecrement).toString(), }); } } //# sourceMappingURL=denonClientHeosCli.js.map