Skip to content

Commit

Permalink
release 4.10.0
Browse files Browse the repository at this point in the history
  • Loading branch information
grzegorz914 committed Mar 4, 2024
1 parent 74d840e commit 6aaec66
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 96 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Note - after update to 4.7.x buttons, sensors, volume display type need to be configure again using config UI.
## Note - after update to 3.15.x need remove the accessory frome Home app and add it again.

## [4.10.0] - (04.03.2024)
## Changes
- added support to subscribe MQTT and control device
- config schema updated
- cleanup

## [4.9.0] - (28.01.2024)
## Note - after update to 4.9.x all buttons (Sound Mode) need to be configure again using config UI.
## Changes
Expand Down
30 changes: 23 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,7 @@ Tested Denon AVR-2112CI, AVR-3311CI, AVR-X6300H, AVR-X2700H, AVC-X4800H, Marantz
* Siri can be used for all functions, some times need create legacy buttons/switches/sensors.
* Automations can be used for all functions, some times need create legacy buttons/switches/sensors.
* This plugin is based upon the official documentation: [Denon Control Protocol 2020](https://github.com/grzegorz914/homebridge-denon-tv/blob/main/doc/Denon%20Control%20Protocol.xlsx)
* RESTful server:
* Request: `http//homebridge_ip_address:port/path`.
* Path: `info`, `state`, `picture`, `surround`.
* Respone as JSON data.
* MQTT client:
* Topic: `Info`, `State`, `Picture`, `Surround`.
* Publish as JSON data.
* Support external integrations, [RESTFul](https://github.com/grzegorz914/homebridge-denon-tv?tab=readme-ov-file#restful-integration), [MQTT](https://github.com/grzegorz914/homebridge-denon-tv?tab=readme-ov-file#mqtt-integration).

<p align="left">
<a href="https://github.com/grzegorz914/homebridge-denon-tv"><img src="https://raw.githubusercontent.com/grzegorz914/homebridge-denon-tv/main/graphics/homekit.png" width="382"></a>
Expand Down Expand Up @@ -123,3 +117,25 @@ Tested Denon AVR-2112CI, AVR-3311CI, AVR-X6300H, AVR-X2700H, AVC-X4800H, Marantz
| `mqttPasswd` | Here set the MQTT Broker password. |
| `mqttDebug` | If enabled, deep log will be present in homebridge console for MQTT. |
| `AV Surround Mode` | This extra Accessory will control all functions of Main Zone except `Inputs` and `Buttons`. |

### RESTFul Integration

* Request: `http//homebridge_ip_address:port/path`.
* Path: `info`, `state`, `picture`, `surround`.
* Respone as JSON data.

### MQTT Integration

| Direction | Topic | Message | Payload Data |
| --- | --- | --- | --- |
| Publish | `Info`, `State`, `Picture`, `Surround` | `{Power: {value: OFF}}` | JSON object. |
| Subscribe | `Set` | `{"Power": true}` | JSON object. |

| Subscribe | Key | Value | Type | Description |
| --- | --- | --- | --- | --- |
| Denon/Marantz | | | | |
| | `Power` | `true`, `false` | boolean | Power state. |
| | `Input` | `SAT/CBL` | string | Set input. |
| | `Surround` | `MUSIC` | string | Set surround mode. |
| | `Volume` | `100` | integer | Set volume. |
| | `Mute` | `true`, `false` | boolean | Set mute. |
8 changes: 4 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@
const path = require('path');
const fs = require('fs');
const DenonDevice = require('./src/denondevice.js');
const CONSTANS = require('./src/constans.json');
const CONSTANTS = require('./src/constants.json');

class DenonPlatform {
constructor(log, config, api) {
// only load if configured
if (!config || !Array.isArray(config.devices)) {
log.warn(`No configuration found for ${CONSTANS.PluginName}`);
log.warn(`No configuration found for ${CONSTANTS.PluginName}`);
return;
}
this.accessories = [];
Expand Down Expand Up @@ -40,7 +40,7 @@ class DenonPlatform {
//denon device
const denonDevice = new DenonDevice(api, prefDir, device);
denonDevice.on('publishAccessory', (accessory) => {
api.publishExternalAccessories(CONSTANS.PluginName, [accessory]);
api.publishExternalAccessories(CONSTANTS.PluginName, [accessory]);
const debug = enableDebugMode ? log(`Device: ${host} ${deviceName}, published as external accessory.`) : false;
})
.on('devInfo', (devInfo) => {
Expand All @@ -65,5 +65,5 @@ class DenonPlatform {
};

module.exports = (api) => {
api.registerPlatform(CONSTANS.PluginName, CONSTANS.PlatformName, DenonPlatform, true);
api.registerPlatform(CONSTANTS.PluginName, CONSTANTS.PlatformName, DenonPlatform, true);
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"displayName": "Denon TV",
"name": "homebridge-denon-tv",
"version": "4.9.17",
"version": "4.10.0",
"description": "Homebridge plugin to control Denon/Marantz AV Receivers.",
"license": "MIT",
"author": "grzegorz914",
Expand Down
File renamed without changes.
58 changes: 28 additions & 30 deletions src/denon.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ const https = require('https');
const axios = require('axios');
const EventEmitter = require('events');
const { XMLParser, XMLBuilder, XMLValidator } = require('fast-xml-parser');
const CONSTANS = require('./constans.json');
const INPUTS_CONVERSION_KEYS = Object.keys(CONSTANS.InputConversion);
const SOUND_MODES_CONVERSION_KEYS = Object.keys(CONSTANS.SoundModeConversion);
const CONSTANTS = require('./constants.json');
const INPUTS_CONVERSION_KEYS = Object.keys(CONSTANTS.InputConversion);
const SOUND_MODES_CONVERSION_KEYS = Object.keys(CONSTANTS.SoundModeConversion);

class DENON extends EventEmitter {
constructor(config) {
Expand All @@ -26,7 +26,7 @@ class DENON extends EventEmitter {
const debugLog = config.debugLog;
const disableLogConnectError = config.disableLogConnectError;
const refreshInterval = config.refreshInterval;
const deviceInfoUrl = [CONSTANS.ApiUrls.DeviceInfoGen0, CONSTANS.ApiUrls.DeviceInfoGen1, CONSTANS.ApiUrls.DeviceInfoGen2][generation];
const deviceInfoUrl = [CONSTANTS.ApiUrls.DeviceInfoGen0, CONSTANTS.ApiUrls.DeviceInfoGen1, CONSTANTS.ApiUrls.DeviceInfoGen2][generation];

this.debugLog = debugLog;
this.refreshInterval = refreshInterval;
Expand Down Expand Up @@ -79,6 +79,7 @@ class DENON extends EventEmitter {
this.pictureMode = 0;
this.soundMode = '';
this.audysseyMode = '';
this.devInfo = {};
const object = {};

this.on('checkDeviceInfo', async () => {
Expand All @@ -88,6 +89,7 @@ class DENON extends EventEmitter {
const parseData = parseString.parse(deviceInfo.data);
const devInfo = [parseData.item, parseData.Device_Info, parseData.Device_Info][generation];
const debug = debugLog ? this.emit('debug', `Info: ${JSON.stringify(devInfo, null, 2)}`) : false;
this.devInfo = devInfo;

//device info
const deviceInfoKeys = Object.keys(devInfo);
Expand Down Expand Up @@ -211,12 +213,6 @@ class DENON extends EventEmitter {
const emitDeviceInfo = this.emitDeviceInfo ? this.emit('deviceInfo', manufacturer, modelName, serialNumber, firmwareRevision, deviceZones, apiVersion, supportPictureMode) : false;
this.emitDeviceInfo = false;

//restFul
this.emit('restFul', 'info', devInfo);

//mqtt
this.emit('mqtt', 'Info', devInfo);

this.supportPictureMode = supportPictureMode;
this.supportSoundMode = supportSoundMode;

Expand All @@ -234,7 +230,7 @@ class DENON extends EventEmitter {
.on('checkState', async () => {
try {
//get zones status
const zoneStateUrl = [CONSTANS.ApiUrls.MainZoneStatusLite, CONSTANS.ApiUrls.Zone2StatusLite, CONSTANS.ApiUrls.Zone3StatusLite, CONSTANS.ApiUrls.SoundModeStatus][zone];
const zoneStateUrl = [CONSTANTS.ApiUrls.MainZoneStatusLite, CONSTANTS.ApiUrls.Zone2StatusLite, CONSTANTS.ApiUrls.Zone3StatusLite, CONSTANTS.ApiUrls.SoundModeStatus][zone];
const deviceState = await this.axiosInstance(zoneStateUrl);
const parseDeviceState = parseString.parse(deviceState.data);
const devState = parseDeviceState.item;
Expand All @@ -243,36 +239,48 @@ class DENON extends EventEmitter {
//get receiver status
const statusKeys = Object.keys(devState);
const power = devState.Power.value === 'ON';
const input = INPUTS_CONVERSION_KEYS.includes(devState.InputFuncSelect.value) ? CONSTANS.InputConversion[devState.InputFuncSelect.value] : devState.InputFuncSelect.value;
const input = INPUTS_CONVERSION_KEYS.includes(devState.InputFuncSelect.value) ? CONSTANTS.InputConversion[devState.InputFuncSelect.value] : devState.InputFuncSelect.value;
const volumeDisplay = statusKeys.includes('VolumeDisplay') ? devState.VolumeDisplay.value : this.volumeDisplay;
const volume = parseFloat(devState.MasterVolume.value) >= -79.5 ? parseInt(devState.MasterVolume.value) + 80 : 0;
const mute = devState.Mute.value === 'on';

//get picture mode
const checkPictureMode = this.supportPictureMode && power && zone === 0;
const devicePictureMode = checkPictureMode ? await this.axiosInstancePost(CONSTANS.ApiUrls.AppCommand, CONSTANS.BodyXml.GetPictureMode) : false;
const devicePictureMode = checkPictureMode ? await this.axiosInstancePost(CONSTANTS.ApiUrls.AppCommand, CONSTANTS.BodyXml.GetPictureMode) : false;
const parseDevicePictureMode = checkPictureMode ? parseString.parse(devicePictureMode.data) : false;
const debug1 = debugLog && checkPictureMode ? this.emit('debug', `Picture mode: ${JSON.stringify(parseDevicePictureMode, null, 2)}`) : false;
const pictureMode = checkPictureMode ? parseDevicePictureMode.rx.cmd.value : this.pictureMode;

//get sound mode
const checkZone = zone === 0 || zone === 3;
const checkSoundeMode = this.supportSoundMode && power && checkZone;
const deviceSoundMode = checkSoundeMode ? await this.axiosInstancePost(CONSTANS.ApiUrls.AppCommand, CONSTANS.BodyXml.GetSurroundModeStatus) : false;
const deviceSoundMode = checkSoundeMode ? await this.axiosInstancePost(CONSTANTS.ApiUrls.AppCommand, CONSTANTS.BodyXml.GetSurroundModeStatus) : false;
const parseDeviceSoundMode = checkSoundeMode ? parseString.parse(deviceSoundMode.data) : false;
const debug2 = debugLog && checkSoundeMode ? this.emit('debug', `Sound mode: ${JSON.stringify(parseDeviceSoundMode, null, 2)}`) : false;
const soundMode = checkSoundeMode ? SOUND_MODES_CONVERSION_KEYS.includes((parseDeviceSoundMode.rx.cmd.surround).replace(/[^a-zA-Z0-9]/g, '').toUpperCase()) ? CONSTANS.SoundModeConversion[(parseDeviceSoundMode.rx.cmd.surround).replace(/[^a-zA-Z0-9]/g, '').toUpperCase()] : (parseDeviceSoundMode.rx.cmd.surround).replace(/[^a-zA-Z0-9]/g, '').toUpperCase() : this.soundMode;
const soundMode = checkSoundeMode ? SOUND_MODES_CONVERSION_KEYS.includes((parseDeviceSoundMode.rx.cmd.surround).replace(/[^a-zA-Z0-9]/g, '').toUpperCase()) ? CONSTANTS.SoundModeConversion[(parseDeviceSoundMode.rx.cmd.surround).replace(/[^a-zA-Z0-9]/g, '').toUpperCase()] : (parseDeviceSoundMode.rx.cmd.surround).replace(/[^a-zA-Z0-9]/g, '').toUpperCase() : this.soundMode;

//get audyssey mode
const checkAudysseyMode = false //power && zone === 0;
const deviceAudysseyMode = checkAudysseyMode ? await this.axiosInstancePost(CONSTANS.ApiUrls.AppCommand, CONSTANS.BodyXml.GetAudyssey) : false;
const deviceAudysseyMode = checkAudysseyMode ? await this.axiosInstancePost(CONSTANTS.ApiUrls.AppCommand, CONSTANTS.BodyXml.GetAudyssey) : false;
const parseDeviceAudysseyMode = checkAudysseyMode ? parseString.parse(deviceAudysseyMode.data) : false;
const debug3 = debugLog && checkAudysseyMode ? this.emit('debug', `Audyssey mode: ${JSON.stringify(parseDeviceAudysseyMode, null, 2)}`) : false;
const sudysseyMode = checkAudysseyMode ? parseDeviceAudysseyMode.rx.cmd.value : this.audysseyMode;

//select reference
const reference = [input, input, input, soundMode][zone];

//restFul
this.emit('restFul', 'info', this.devInfo);
this.emit('restFul', 'state', devState);
const restFul1 = checkPictureMode ? this.emit('restFul', 'picture', { 'Picture Mode': CONSTANTS.PictureModesDenonNumber[pictureMode] }) : false;
const restFul2 = checkSoundeMode ? this.emit('restFul', 'surround', { 'Sound Mode': soundMode }) : false;

//mqtt
this.emit('mqtt', 'Info', this.devInfo);
this.emit('mqtt', 'State', devState);
const mqtt2 = checkPictureMode ? this.emit('mqtt', 'Picture', { 'Picture Mode': CONSTANTS.PictureModesDenonNumber[pictureMode] }) : false;
const mqtt3 = checkSoundeMode ? this.emit('mqtt', 'Surround', { 'Sound Mode': soundMode }) : false;

//update only if value change
if (power === this.power && reference === this.reference && volume === this.volume && volumeDisplay === this.volumeDisplay && mute === this.mute && pictureMode === this.pictureMode && soundMode === this.soundMode) {
this.checkState();
Expand All @@ -291,16 +299,6 @@ class DENON extends EventEmitter {
//emit state changed
this.emit('stateChanged', power, reference, volume, volumeDisplay, mute, pictureMode);

//restFul
this.emit('restFul', 'state', devState);
const restFul1 = checkPictureMode ? this.emit('restFul', 'picture', { 'Picture Mode': CONSTANS.PictureModesDenonNumber[pictureMode] }) : false;
const restFul2 = checkSoundeMode ? this.emit('restFul', 'surround', { 'Sound Mode': CONSTANS.SoundModeConversion[soundMode] }) : false;

//mqtt
this.emit('mqtt', 'State', devState);
const mqtt2 = checkPictureMode ? this.emit('mqtt', 'Picture', { 'Picture Mode': CONSTANS.PictureModesDenonNumber[pictureMode] }) : false;
const mqtt3 = checkSoundeMode ? this.emit('mqtt', 'Surround', { 'Sound Mode': CONSTANS.SoundModeConversion[soundMode] }) : false;

this.checkState();
} catch (error) {
const debug = disableLogConnectError ? false : this.emit('error', `State error: ${error}, reconnect in ${this.refreshInterval}s.`);
Expand Down Expand Up @@ -402,10 +400,10 @@ class DENON extends EventEmitter {
const debug = !this.debugLog ? false : this.emit('message', `temp Inputs: ${JSON.stringify(tempInputs, null, 2)}`);
for (const input of tempInputs) {
const inputName = input.name;
const inputReference = INPUTS_CONVERSION_KEYS.includes(input.reference) ? CONSTANS.InputConversion[input.reference] : input.reference;
const inputReference = INPUTS_CONVERSION_KEYS.includes(input.reference) ? CONSTANTS.InputConversion[input.reference] : input.reference;
const inputReferenceSubstring = inputReference.substring(0, 5) ?? 'Unknown';
const inputModeExist = inputReferenceSubstring in CONSTANS.InputMode;
const inputMode = zone <= 2 ? inputModeExist ? CONSTANS.InputMode[inputReferenceSubstring] : 'SI' : 'MS';
const inputModeExist = inputReferenceSubstring in CONSTANTS.InputMode;
const inputMode = zone <= 2 ? inputModeExist ? CONSTANTS.InputMode[inputReferenceSubstring] : 'SI' : 'MS';
const obj = {
'name': inputName,
'reference': inputReference,
Expand All @@ -431,7 +429,7 @@ class DENON extends EventEmitter {
send(command) {
return new Promise(async (resolve, reject) => {
try {
const path = CONSTANS.ApiUrls.iPhoneDirect + command;
const path = CONSTANTS.ApiUrls.iPhoneDirect + command;
await this.axiosInstance(path);

await new Promise(resolve => setTimeout(resolve, 250));
Expand Down
Loading

0 comments on commit 6aaec66

Please sign in to comment.