From b3b2dd1e3955cb88e7294cba037b5f659fe70147 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Fu=CC=88chslin?= Date: Sun, 21 Jul 2019 17:44:36 +0200 Subject: [PATCH 01/12] Use denon-client InputOptions to get list of inputs The Denon http API does not work on Heos-enabled AVR devices. --- app.js | 36 ++++++++---------------------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/app.js b/app.js index ab6c822..cb739e4 100644 --- a/app.js +++ b/app.js @@ -115,34 +115,14 @@ function probeInputs(settings) { } function queryInputs(hostname) { - return fetch('http://' + hostname + '/goform/formMainZone_MainZoneXmlStatus.xml',{timeout: 2000}) - .then(res => { - return res.text()}) - .then(body => { - - var result = parse.parse(body); - var inputs = result['item']['InputFuncList']['value']; - var renames = result['item']['RenameSource']['value']; - - var outs = (result.item.SourceDelete ? Promise.resolve(result.item.SourceDelete.value) : - - fetch('http://' + hostname + '/goform/formMainZone_MainZoneXml.xml',{timeout: 2000}) - .then(res => res.text()) - .then(body => { - let r = parse.parse(body); - return r['item']['SourceDelete']['value']; - }) - ) - .then((removes) => { - return inputs.map((x, i) => { - var dict = {}; - dict["title"] = renames[i].value ? renames[i].value : renames[i]; - dict["value"] = x; - return dict; - }).filter((data, index) => removes[index] == "USE" && data.value != "TV"); - }); - return outs; - }); + return Promise.resolve( + Object.keys(Denon.Options.InputOptions) + .filter(title => title != 'Status') + .sort() + .map(title => { + return { title, value: Denon.Options.InputOptions[title] } + }) + ); } function setup_denon_connection(host) { From 4296d5ad438a51f3e16eba4c3d6f2156f03af813 Mon Sep 17 00:00:00 2001 From: Jason Charrier Date: Mon, 25 Oct 2021 14:05:16 -0500 Subject: [PATCH 02/12] updated node-roon-api dependency. Fixes "this.moo.close is not a function" error. --- app.js | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index cb739e4..4fbc054 100644 --- a/app.js +++ b/app.js @@ -15,7 +15,7 @@ var denon = {}; var roon = new RoonApi({ extension_id: 'org.pruessmann.roon.denon', display_name: 'Denon/Marantz AVR', - display_version: '0.0.9', + display_version: '0.0.10', publisher: 'Doc Bobo', email: 'boris@pruessmann.org', website: 'https://github.com/docbobo/roon-extension-denon', diff --git a/package.json b/package.json index 2ec6389..444e581 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "denon-client": "^0.2.0", "fast-xml-parser": "^3.9.10", "node-fetch": "^2.1.2", - "node-roon-api": "github:roonlabs/node-roon-api#fc06320225d8f919663a785e387363e441f46261", + "node-roon-api": "github:roonlabs/node-roon-api#b09c875738360a9413518a8a51ac70294745a926", "node-roon-api-settings": "github:roonlabs/node-roon-api-settings#67cd8ca156c5bcd01ea63833ceaaec6d6a79654d", "node-roon-api-source-control": "github:roonlabs/node-roon-api-source-control#fab2ba33f2c9249a8c9e69b6dcccfc8f333ab12e", "node-roon-api-status": "github:roonlabs/node-roon-api-status#504c918d6da267e03fbb4337befa71ca3d3c7526", From e9c2c03aabdc7940f2dd1a2e839be97afcdbfa25 Mon Sep 17 00:00:00 2001 From: Arthur Soares Date: Tue, 8 Mar 2022 18:35:51 +0100 Subject: [PATCH 03/12] Update app.js Bumped version --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index 4fbc054..bfef4dc 100644 --- a/app.js +++ b/app.js @@ -15,7 +15,7 @@ var denon = {}; var roon = new RoonApi({ extension_id: 'org.pruessmann.roon.denon', display_name: 'Denon/Marantz AVR', - display_version: '0.0.10', + display_version: '0.0.11', publisher: 'Doc Bobo', email: 'boris@pruessmann.org', website: 'https://github.com/docbobo/roon-extension-denon', From 7a223ef1078f234ad30409578f51ae93247f2f7f Mon Sep 17 00:00:00 2001 From: Arthur Soares Date: Tue, 8 Mar 2022 18:36:24 +0100 Subject: [PATCH 04/12] Update package.json Bumped `denon-client` to fix ZONE2 turning on. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 444e581..78b39c7 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "license": "Apache-2.0", "dependencies": { "debug": "^3.1.0", - "denon-client": "^0.2.0", + "denon-client": "^0.2.4", "fast-xml-parser": "^3.9.10", "node-fetch": "^2.1.2", "node-roon-api": "github:roonlabs/node-roon-api#b09c875738360a9413518a8a51ac70294745a926", From c437778c4c6222188569cfcf9d5c21f342b10cc5 Mon Sep 17 00:00:00 2001 From: Jason Charrier Date: Thu, 9 Feb 2023 11:41:24 -0600 Subject: [PATCH 05/12] updated node-roon-api and bumped version numbers --- app.js | 145 ++++++++++++++++++++++++++------------------------- package.json | 36 ++++++------- 2 files changed, 91 insertions(+), 90 deletions(-) diff --git a/app.js b/app.js index bfef4dc..15882b5 100644 --- a/app.js +++ b/app.js @@ -1,59 +1,58 @@ "use strict"; -var debug = require('debug')('roon-extension-denon'), - debug_keepalive = require('debug')('roon-extension-denon:keepalive'), - Denon = require('denon-client'), - RoonApi = require('node-roon-api'), - RoonApiSettings = require('node-roon-api-settings'), - RoonApiStatus = require('node-roon-api-status'), +var debug = require('debug')('roon-extension-denon'), + debug_keepalive = require('debug')('roon-extension-denon:keepalive'), + Denon = require('denon-client'), + RoonApi = require('node-roon-api'), + RoonApiSettings = require('node-roon-api-settings'), + RoonApiStatus = require('node-roon-api-status'), RoonApiVolumeControl = require('node-roon-api-volume-control'), RoonApiSourceControl = require('node-roon-api-source-control'), - fetch = require('node-fetch'), - parse = require('fast-xml-parser'); + fetch = require('node-fetch'), + parse = require('fast-xml-parser'); var denon = {}; var roon = new RoonApi({ - extension_id: 'org.pruessmann.roon.denon', - display_name: 'Denon/Marantz AVR', - display_version: '0.0.11', - publisher: 'Doc Bobo', - email: 'boris@pruessmann.org', - website: 'https://github.com/docbobo/roon-extension-denon', + extension_id: 'org.pruessmann.roon.denon', + display_name: 'Denon/Marantz AVR', + display_version: '0.0.12', + publisher: 'Doc Bobo', + email: 'boris@pruessmann.org', + website: 'https://github.com/docbobo/roon-extension-denon', }); var mysettings = roon.load_config("settings") || { - hostname: "", + hostname: "", setsource: "", }; function make_layout(settings) { var l = { - values: settings, - layout: [], + values: settings, + layout: [], has_error: false }; l.layout.push({ - type: "string", - title: "Host name or IP Address", - subtitle: "The IP address or hostname of the Denon/Marantz receiver.", + type: "string", + title: "Host name or IP Address", + subtitle: "The IP address or hostname of the Denon/Marantz receiver.", maxlength: 256, - setting: "hostname", + setting: "hostname", }); - if(settings.err) { + if (settings.err) { l.has_error = true; l.layout.push({ - type: "status", - title: settings.err, + type: "status", + title: settings.err, }); - } - else { + } else { l.has_error = false; - if(settings.hostname) { + if (settings.hostname) { l.layout.push({ - type: "dropdown", - title: "Input", - values: settings.inputs, + type: "dropdown", + title: "Input", + values: settings.inputs, setting: "setsource", }); } @@ -76,7 +75,7 @@ var svc_settings = new RoonApiSettings(roon, { req.send_complete(l.has_error ? "NotValid" : "Success", { settings: l }); delete settings.inputs; - if(!l.has_error && !isdryrun) { + if (!l.has_error && !isdryrun) { var old_hostname = mysettings.hostname; var old_setsource = mysettings.setsource; mysettings = l.values; @@ -93,7 +92,7 @@ var svc_volume_control = new RoonApiVolumeControl(roon); var svc_source_control = new RoonApiSourceControl(roon); roon.init_services({ - provided_services: [ svc_status, svc_settings, svc_volume_control, svc_source_control ] + provided_services: [svc_status, svc_settings, svc_volume_control, svc_source_control] }); function probeInputs(settings) { @@ -105,7 +104,7 @@ function probeInputs(settings) { settings.inputs = inputs }) : Promise.resolve()) - .catch(err => { + .catch(err => { settings.err = err.message; }) .then(() => { @@ -117,19 +116,22 @@ function probeInputs(settings) { function queryInputs(hostname) { return Promise.resolve( Object.keys(Denon.Options.InputOptions) - .filter(title => title != 'Status') - .sort() - .map(title => { - return { title, value: Denon.Options.InputOptions[title] } - }) - ); + .filter(title => title != 'Status') + .sort() + .map(title => { + return { title, value: Denon.Options.InputOptions[title] } + }) + ); } function setup_denon_connection(host) { debug("setup_denon_connection (" + host + ")"); - if (denon.keepalive) { clearInterval(denon.keepalive); denon.keepalive = null; } - if (denon.client) { denon.client.removeAllListeners('close'); denon.client.disconnect(); delete(denon.client); } + if (denon.keepalive) {  clearInterval(denon.keepalive); + denon.keepalive = null; } + if (denon.client) { denon.client.removeAllListeners('close'); + denon.client.disconnect(); + delete(denon.client); } if (!host) { svc_status.set_status("Not configured, please check settings.", true); @@ -159,8 +161,8 @@ function setup_denon_connection(host) { denon.client.on('close', (had_error) => { debug('Received onClose(%O): Reconnecting...', had_error); - if(denon.client) { - svc_status.set_status("Connection closed by receiver. Reconnecting...", true); + if (denon.client) { + svc_status.set_status("Connection closed by receiver. Reconnecting...", true); setTimeout(() => { connect(); }, 1000); @@ -178,7 +180,7 @@ function setup_denon_connection(host) { let stat = check_status(denon.source_state.Power, denon.source_state.Input); debug("Power differs - updating"); if (denon.source_control) { - denon.source_control.update_state( {status: stat}); + denon.source_control.update_state({ status: stat }); } } }); @@ -192,7 +194,7 @@ function setup_denon_connection(host) { let stat = check_status(denon.source_state.Power, denon.source_state.Input); debug("input differs - updating"); if (denon.source_control) { - denon.source_control.update_state( {status: stat}); + denon.source_control.update_state({ status: stat }); } } @@ -239,18 +241,18 @@ function setup_denon_connection(host) { function connect() { denon.client.connect() - .then(() => create_volume_control(denon)) - .then(() => mysettings.setsource ? create_source_control(denon) : Promise.resolve()) - .then(() => { - svc_status.set_status("Connected to receiver", false); - }) - .catch((error) => { - debug("setup_denon_connection: Error during setup. Retrying..."); - - // TODO: Fix error message - console.log(error); - svc_status.set_status("Could not connect receiver: " + error, true); - }); + .then(() => create_volume_control(denon)) + .then(() => mysettings.setsource ? create_source_control(denon) : Promise.resolve()) + .then(() => { + svc_status.set_status("Connected to receiver", false); + }) + .catch((error) => { + debug("setup_denon_connection: Error during setup. Retrying..."); + + // TODO: Fix error message + console.log(error); + svc_status.set_status("Could not connect receiver: " + error, true); + }); } function check_status(power, input) { @@ -262,8 +264,7 @@ function check_status(power, input) { } else { stat = "deselected"; } - } - else { + } else { stat = "standby"; } debug("Receiver Status: %s", stat); @@ -272,23 +273,23 @@ function check_status(power, input) { function create_volume_control(denon) { debug("create_volume_control: volume_control=%o", denon.volume_control) - if(!denon.volume_control) { + if (!denon.volume_control) { denon.volume_state = { display_name: "Main Zone", - volume_type: "db", - volume_min: -79.5, - volume_step: 0.5, + volume_type: "db", + volume_min: -79.5, + volume_step: 0.5, }; var device = { state: denon.volume_state, control_key: 1, - set_volume: function (req, mode, value) { + set_volume: function(req, mode, value) { debug("set_volume: mode=%s value=%d", mode, value); let newvol = mode == "absolute" ? value : (state.volume_value + value); - if (newvol < this.state.volume_min) newvol = this.state.volume_min; + if (newvol < this.state.volume_min) newvol = this.state.volume_min; else if (newvol > this.state.volume_max) newvol = this.state.volume_max; denon.client.setVolume(newvol + 80).then(() => { @@ -301,7 +302,7 @@ function create_volume_control(denon) { req.send_complete("Failed"); }); }, - set_mute: function (req, inAction) { + set_mute: function(req, inAction) { debug("set_mute: action=%s", inAction); const action = !this.state.is_muted ? "on" : "off"; @@ -339,7 +340,7 @@ function create_volume_control(denon) { function create_source_control(denon) { debug("create_source_control: source_control=%o", denon.source_control) - if(!denon.source_control) { + if (!denon.source_control) { denon.source_state = { display_name: "Main Zone", supports_standby: true, @@ -351,8 +352,8 @@ function create_source_control(denon) { var device = { state: denon.source_state, control_key: 2, - - convenience_switch: function (req) { + + convenience_switch: function(req) { if (denon.source_state.Power === "STANDBY") { denon.client.setPower('ON'); } @@ -368,7 +369,7 @@ function create_source_control(denon) { }); } }, - standby: function (req) { + standby: function(req) { denon.client.getPower().then((val) => { denon.client.setPower(val === 'STANDBY' ? "ON" : "STANDBY").then(() => { req.send_complete("Success"); @@ -389,7 +390,7 @@ function create_source_control(denon) { }).then((val) => { denon.source_state.Input = val; denon.source_state.status = check_status(denon.source_state.Power, denon.source_state.Input); - if(denon.source_control) { + if (denon.source_control) { denon.source_control.update_state(denon.source_state); } else { debug("Registering source control extension"); @@ -401,4 +402,4 @@ function create_source_control(denon) { setup_denon_connection(mysettings.hostname); -roon.start_discovery(); +roon.start_discovery(); \ No newline at end of file diff --git a/package.json b/package.json index 78b39c7..826447e 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,19 @@ { - "name": "roon-extension-denon", - "version": "0.0.9", - "description": "Volume Control extension to control Denon/Marantz AV receivers via network.", - "main": "app.js", - "author": "Doc Bobo (https://blog.pruessmann.org/)", - "license": "Apache-2.0", - "dependencies": { - "debug": "^3.1.0", - "denon-client": "^0.2.4", - "fast-xml-parser": "^3.9.10", - "node-fetch": "^2.1.2", - "node-roon-api": "github:roonlabs/node-roon-api#b09c875738360a9413518a8a51ac70294745a926", - "node-roon-api-settings": "github:roonlabs/node-roon-api-settings#67cd8ca156c5bcd01ea63833ceaaec6d6a79654d", - "node-roon-api-source-control": "github:roonlabs/node-roon-api-source-control#fab2ba33f2c9249a8c9e69b6dcccfc8f333ab12e", - "node-roon-api-status": "github:roonlabs/node-roon-api-status#504c918d6da267e03fbb4337befa71ca3d3c7526", - "node-roon-api-volume-control": "github:roonlabs/node-roon-api-volume-control#56315d95344c9e0dc98c07c30a2de08727437b1e" - } -} + "name": "roon-extension-denon", + "version": "0.0.12", + "description": "Volume Control extension to control Denon/Marantz AV receivers via network.", + "main": "app.js", + "author": "Doc Bobo (https://blog.pruessmann.org/)", + "license": "Apache-2.0", + "dependencies": { + "debug": "^3.1.0", + "denon-client": "^0.2.4", + "fast-xml-parser": "^3.9.10", + "node-fetch": "^2.1.2", + "node-roon-api": "github:roonlabs/node-roon-api#22e20e4981e5dac7bbfb211009d5006c7c87f567", + "node-roon-api-settings": "github:roonlabs/node-roon-api-settings#67cd8ca156c5bcd01ea63833ceaaec6d6a79654d", + "node-roon-api-source-control": "github:roonlabs/node-roon-api-source-control#fab2ba33f2c9249a8c9e69b6dcccfc8f333ab12e", + "node-roon-api-status": "github:roonlabs/node-roon-api-status#504c918d6da267e03fbb4337befa71ca3d3c7526", + "node-roon-api-volume-control": "github:roonlabs/node-roon-api-volume-control#56315d95344c9e0dc98c07c30a2de08727437b1e" + } +} \ No newline at end of file From ea4fe6319a9690e22d544dbb8ccb937170383311 Mon Sep 17 00:00:00 2001 From: Jason Charrier Date: Tue, 26 Dec 2023 22:28:27 -0600 Subject: [PATCH 06/12] updated roon api. Bump version. --- app.js | 14 +++++++++----- package.json | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index 15882b5..1d3c42d 100644 --- a/app.js +++ b/app.js @@ -15,7 +15,7 @@ var denon = {}; var roon = new RoonApi({ extension_id: 'org.pruessmann.roon.denon', display_name: 'Denon/Marantz AVR', - display_version: '0.0.12', + display_version: '0.0.13', publisher: 'Doc Bobo', email: 'boris@pruessmann.org', website: 'https://github.com/docbobo/roon-extension-denon', @@ -127,11 +127,15 @@ function queryInputs(hostname) { function setup_denon_connection(host) { debug("setup_denon_connection (" + host + ")"); - if (denon.keepalive) {  clearInterval(denon.keepalive); - denon.keepalive = null; } - if (denon.client) { denon.client.removeAllListeners('close'); + if (denon.keepalive) {  + clearInterval(denon.keepalive); + denon.keepalive = null; + } + if (denon.client) { + denon.client.removeAllListeners('close'); denon.client.disconnect(); - delete(denon.client); } + delete(denon.client); + } if (!host) { svc_status.set_status("Not configured, please check settings.", true); diff --git a/package.json b/package.json index 826447e..ebb36ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roon-extension-denon", - "version": "0.0.12", + "version": "0.0.13", "description": "Volume Control extension to control Denon/Marantz AV receivers via network.", "main": "app.js", "author": "Doc Bobo (https://blog.pruessmann.org/)", @@ -10,7 +10,7 @@ "denon-client": "^0.2.4", "fast-xml-parser": "^3.9.10", "node-fetch": "^2.1.2", - "node-roon-api": "github:roonlabs/node-roon-api#22e20e4981e5dac7bbfb211009d5006c7c87f567", + "node-roon-api": "github:roonlabs/node-roon-api#7cfaddc63b0d8a8bffe0f71df02d7c73adc728a5", "node-roon-api-settings": "github:roonlabs/node-roon-api-settings#67cd8ca156c5bcd01ea63833ceaaec6d6a79654d", "node-roon-api-source-control": "github:roonlabs/node-roon-api-source-control#fab2ba33f2c9249a8c9e69b6dcccfc8f333ab12e", "node-roon-api-status": "github:roonlabs/node-roon-api-status#504c918d6da267e03fbb4337befa71ca3d3c7526", From 8aa07b9ef59ce256af695a20becb4c05b724cef5 Mon Sep 17 00:00:00 2001 From: Jason Charrier Date: Thu, 16 Jan 2025 23:23:41 -0600 Subject: [PATCH 07/12] added support for Node 22 courtesy of OonihiloO's patch --- app.js | 2 +- package.json | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index 1d3c42d..0c6690d 100644 --- a/app.js +++ b/app.js @@ -15,7 +15,7 @@ var denon = {}; var roon = new RoonApi({ extension_id: 'org.pruessmann.roon.denon', display_name: 'Denon/Marantz AVR', - display_version: '0.0.13', + display_version: '0.0.14', publisher: 'Doc Bobo', email: 'boris@pruessmann.org', website: 'https://github.com/docbobo/roon-extension-denon', diff --git a/package.json b/package.json index ebb36ba..cf7cf94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roon-extension-denon", - "version": "0.0.13", + "version": "0.0.14", "description": "Volume Control extension to control Denon/Marantz AV receivers via network.", "main": "app.js", "author": "Doc Bobo (https://blog.pruessmann.org/)", @@ -10,10 +10,10 @@ "denon-client": "^0.2.4", "fast-xml-parser": "^3.9.10", "node-fetch": "^2.1.2", - "node-roon-api": "github:roonlabs/node-roon-api#7cfaddc63b0d8a8bffe0f71df02d7c73adc728a5", + "node-roon-api": "github:OonihiloO/node-roon-api#e0a51b72795fe9921d7b2b12d9c0102a475faa60", "node-roon-api-settings": "github:roonlabs/node-roon-api-settings#67cd8ca156c5bcd01ea63833ceaaec6d6a79654d", "node-roon-api-source-control": "github:roonlabs/node-roon-api-source-control#fab2ba33f2c9249a8c9e69b6dcccfc8f333ab12e", "node-roon-api-status": "github:roonlabs/node-roon-api-status#504c918d6da267e03fbb4337befa71ca3d3c7526", "node-roon-api-volume-control": "github:roonlabs/node-roon-api-volume-control#56315d95344c9e0dc98c07c30a2de08727437b1e" } -} \ No newline at end of file +} From a779d0cbaa9252ab5836c058d4190fe3695327ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Pr=C3=BC=C3=9Fmann?= Date: Fri, 17 Jan 2025 09:33:24 +0100 Subject: [PATCH 08/12] updated extension metadata --- app.js | 7 +++---- package.json | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index 0c6690d..373f968 100644 --- a/app.js +++ b/app.js @@ -15,9 +15,9 @@ var denon = {}; var roon = new RoonApi({ extension_id: 'org.pruessmann.roon.denon', display_name: 'Denon/Marantz AVR', - display_version: '0.0.14', + display_version: '2025.1.1', publisher: 'Doc Bobo', - email: 'boris@pruessmann.org', + email: 'docbobo@pm.me', website: 'https://github.com/docbobo/roon-extension-denon', }); @@ -60,7 +60,6 @@ function make_layout(settings) { return l; } - var svc_settings = new RoonApiSettings(roon, { get_settings: function(cb) { probeInputs(mysettings) @@ -406,4 +405,4 @@ function create_source_control(denon) { setup_denon_connection(mysettings.hostname); -roon.start_discovery(); \ No newline at end of file +roon.start_discovery(); diff --git a/package.json b/package.json index cf7cf94..5f7114c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "roon-extension-denon", - "version": "0.0.14", + "version": "2025.1.1", "description": "Volume Control extension to control Denon/Marantz AV receivers via network.", "main": "app.js", "author": "Doc Bobo (https://blog.pruessmann.org/)", From cf6686145ec3e1e4c59b34430f7f385a203e33e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Pr=C3=BC=C3=9Fmann?= Date: Fri, 17 Jan 2025 09:34:11 +0100 Subject: [PATCH 09/12] reformatted code, added .editorconfig --- .editorconfig | 11 ++ app.js | 348 +++++++++++++++++++++++++++++--------------------- 2 files changed, 214 insertions(+), 145 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7d1f046 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +# Generated by Home Manager +root=true + +[*] +charset=utf-8 +end_of_line=lf +indent_size=4 +indent_style=space +insert_final_newline=true +trim_trailing_whitespace=true + diff --git a/app.js b/app.js index 373f968..e5248b2 100644 --- a/app.js +++ b/app.js @@ -1,24 +1,24 @@ "use strict"; -var debug = require('debug')('roon-extension-denon'), - debug_keepalive = require('debug')('roon-extension-denon:keepalive'), - Denon = require('denon-client'), - RoonApi = require('node-roon-api'), - RoonApiSettings = require('node-roon-api-settings'), - RoonApiStatus = require('node-roon-api-status'), - RoonApiVolumeControl = require('node-roon-api-volume-control'), - RoonApiSourceControl = require('node-roon-api-source-control'), - fetch = require('node-fetch'), - parse = require('fast-xml-parser'); +var debug = require("debug")("roon-extension-denon"), + debug_keepalive = require("debug")("roon-extension-denon:keepalive"), + Denon = require("denon-client"), + RoonApi = require("node-roon-api"), + RoonApiSettings = require("node-roon-api-settings"), + RoonApiStatus = require("node-roon-api-status"), + RoonApiVolumeControl = require("node-roon-api-volume-control"), + RoonApiSourceControl = require("node-roon-api-source-control"), + fetch = require("node-fetch"), + parse = require("fast-xml-parser"); var denon = {}; var roon = new RoonApi({ - extension_id: 'org.pruessmann.roon.denon', - display_name: 'Denon/Marantz AVR', - display_version: '2025.1.1', - publisher: 'Doc Bobo', - email: 'docbobo@pm.me', - website: 'https://github.com/docbobo/roon-extension-denon', + extension_id: "org.pruessmann.roon.denon", + display_name: "Denon/Marantz AVR", + display_version: "2025.1.1", + publisher: "Doc Bobo", + email: "docbobo@pm.me", + website: "https://github.com/docbobo/roon-extension-denon", }); var mysettings = roon.load_config("settings") || { @@ -30,7 +30,7 @@ function make_layout(settings) { var l = { values: settings, layout: [], - has_error: false + has_error: false, }; l.layout.push({ @@ -61,29 +61,33 @@ function make_layout(settings) { } var svc_settings = new RoonApiSettings(roon, { - get_settings: function(cb) { - probeInputs(mysettings) - .then((settings) => { - cb(make_layout(settings)); - }); + get_settings: function (cb) { + probeInputs(mysettings).then((settings) => { + cb(make_layout(settings)); + }); }, - save_settings: function(req, isdryrun, settings) { - probeInputs(settings.values) - .then((settings) => { - let l = make_layout(settings); - req.send_complete(l.has_error ? "NotValid" : "Success", { settings: l }); - delete settings.inputs; - - if (!l.has_error && !isdryrun) { - var old_hostname = mysettings.hostname; - var old_setsource = mysettings.setsource; - mysettings = l.values; - svc_settings.update_settings(l); - if (old_hostname != mysettings.hostname || old_setsource != mysettings.setsource) setup_denon_connection(mysettings.hostname); - roon.save_config("settings", mysettings); - } + save_settings: function (req, isdryrun, settings) { + probeInputs(settings.values).then((settings) => { + let l = make_layout(settings); + req.send_complete(l.has_error ? "NotValid" : "Success", { + settings: l, }); - } + delete settings.inputs; + + if (!l.has_error && !isdryrun) { + var old_hostname = mysettings.hostname; + var old_setsource = mysettings.setsource; + mysettings = l.values; + svc_settings.update_settings(l); + if ( + old_hostname != mysettings.hostname || + old_setsource != mysettings.setsource + ) + setup_denon_connection(mysettings.hostname); + roon.save_config("settings", mysettings); + } + }); + }, }); var svc_status = new RoonApiStatus(roon); @@ -91,19 +95,25 @@ var svc_volume_control = new RoonApiVolumeControl(roon); var svc_source_control = new RoonApiSourceControl(roon); roon.init_services({ - provided_services: [svc_status, svc_settings, svc_volume_control, svc_source_control] + provided_services: [ + svc_status, + svc_settings, + svc_volume_control, + svc_source_control, + ], }); function probeInputs(settings) { - - let inputs = (settings.hostname ? - queryInputs(settings.hostname) - .then(inputs => { - delete settings.err; - settings.inputs = inputs - }) : Promise.resolve()) - - .catch(err => { + let inputs = ( + settings.hostname + ? queryInputs(settings.hostname).then((inputs) => { + delete settings.err; + settings.inputs = inputs; + }) + : Promise.resolve() + ) + + .catch((err) => { settings.err = err.message; }) .then(() => { @@ -115,25 +125,25 @@ function probeInputs(settings) { function queryInputs(hostname) { return Promise.resolve( Object.keys(Denon.Options.InputOptions) - .filter(title => title != 'Status') - .sort() - .map(title => { - return { title, value: Denon.Options.InputOptions[title] } - }) + .filter((title) => title != "Status") + .sort() + .map((title) => { + return { title, value: Denon.Options.InputOptions[title] }; + }), ); } function setup_denon_connection(host) { debug("setup_denon_connection (" + host + ")"); - if (denon.keepalive) {  + if (denon.keepalive) { clearInterval(denon.keepalive); denon.keepalive = null; } if (denon.client) { - denon.client.removeAllListeners('close'); + denon.client.removeAllListeners("close"); denon.client.disconnect(); - delete(denon.client); + delete denon.client; } if (!host) { @@ -146,41 +156,50 @@ function setup_denon_connection(host) { denon.client.socket.setTimeout(0); denon.client.socket.setKeepAlive(true, 10000); - denon.client.socket.on('error', (error) => { + denon.client.socket.on("error", (error) => { // Handler for debugging purposes. No need to reconnect since the event will be followed by a close event, // according to documentation. - debug('Received onError(%O)', error); + debug("Received onError(%O)", error); }); - denon.client.on('data', (data) => { + denon.client.on("data", (data) => { debug("%s", data); }); - denon.client.socket.on('timeout', () => { - debug('Received onTimeout(): Closing connection...'); + denon.client.socket.on("timeout", () => { + debug("Received onTimeout(): Closing connection..."); denon.client.disconnect(); }); - denon.client.on('close', (had_error) => { - debug('Received onClose(%O): Reconnecting...', had_error); + denon.client.on("close", (had_error) => { + debug("Received onClose(%O): Reconnecting...", had_error); if (denon.client) { - svc_status.set_status("Connection closed by receiver. Reconnecting...", true); + svc_status.set_status( + "Connection closed by receiver. Reconnecting...", + true, + ); setTimeout(() => { connect(); }, 1000); } else { - svc_status.set_status("Not configured, please check settings.", true); + svc_status.set_status( + "Not configured, please check settings.", + true, + ); } }); - denon.client.on('powerChanged', (val) => { + denon.client.on("powerChanged", (val) => { debug("powerChanged: val=%s", val); let old_power_value = denon.source_state.Power; denon.source_state.Power = val; if (old_power_value != denon.source_state.Power) { - let stat = check_status(denon.source_state.Power, denon.source_state.Input); + let stat = check_status( + denon.source_state.Power, + denon.source_state.Input, + ); debug("Power differs - updating"); if (denon.source_control) { denon.source_control.update_state({ status: stat }); @@ -188,45 +207,53 @@ function setup_denon_connection(host) { } }); - denon.client.on('inputChanged', (val) => { + denon.client.on("inputChanged", (val) => { debug("inputChanged: val=%s", val); let old_Input = denon.source_state.Input; denon.source_state.Input = val; if (old_Input != denon.source_state.Input) { - let stat = check_status(denon.source_state.Power, denon.source_state.Input); + let stat = check_status( + denon.source_state.Power, + denon.source_state.Input, + ); debug("input differs - updating"); if (denon.source_control) { denon.source_control.update_state({ status: stat }); } - } }); - denon.client.on('muteChanged', (val) => { + denon.client.on("muteChanged", (val) => { debug("muteChanged: val=%s", val); denon.volume_state.is_muted = val === Denon.Options.MuteOptions.On; if (denon.volume_control) { - denon.volume_control.update_state({ is_muted: denon.volume_state.is_muted }); + denon.volume_control.update_state({ + is_muted: denon.volume_state.is_muted, + }); } }); - denon.client.on('masterVolumeChanged', (val) => { + denon.client.on("masterVolumeChanged", (val) => { debug("masterVolumeChanged: val=%s", val - 80); denon.volume_state.volume_value = val - 80; if (denon.volume_control) { - denon.volume_control.update_state({ volume_value: denon.volume_state.volume_value }); + denon.volume_control.update_state({ + volume_value: denon.volume_state.volume_value, + }); } }); - denon.client.on('masterVolumeMaxChanged', (val) => { + denon.client.on("masterVolumeMaxChanged", (val) => { debug("masterVolumeMaxChanged: val=%s", val - 80); denon.volume_state.volume_max = val - 80; if (denon.volume_control) { - denon.volume_control.update_state({ volume_max: denon.volume_state.volume_max }); + denon.volume_control.update_state({ + volume_max: denon.volume_state.volume_max, + }); } }); @@ -242,10 +269,14 @@ function setup_denon_connection(host) { } function connect() { - - denon.client.connect() + denon.client + .connect() .then(() => create_volume_control(denon)) - .then(() => mysettings.setsource ? create_source_control(denon) : Promise.resolve()) + .then(() => + mysettings.setsource + ? create_source_control(denon) + : Promise.resolve(), + ) .then(() => { svc_status.set_status("Connected to receiver", false); }) @@ -259,7 +290,6 @@ function connect() { } function check_status(power, input) { - let stat = ""; if (power == "ON") { if (input == mysettings.setsource) { @@ -275,7 +305,7 @@ function check_status(power, input) { } function create_volume_control(denon) { - debug("create_volume_control: volume_control=%o", denon.volume_control) + debug("create_volume_control: volume_control=%o", denon.volume_control); if (!denon.volume_control) { denon.volume_state = { display_name: "Main Zone", @@ -288,118 +318,146 @@ function create_volume_control(denon) { state: denon.volume_state, control_key: 1, - set_volume: function(req, mode, value) { + set_volume: function (req, mode, value) { debug("set_volume: mode=%s value=%d", mode, value); - let newvol = mode == "absolute" ? value : (state.volume_value + value); - if (newvol < this.state.volume_min) newvol = this.state.volume_min; - else if (newvol > this.state.volume_max) newvol = this.state.volume_max; + let newvol = + mode == "absolute" ? value : state.volume_value + value; + if (newvol < this.state.volume_min) + newvol = this.state.volume_min; + else if (newvol > this.state.volume_max) + newvol = this.state.volume_max; - denon.client.setVolume(newvol + 80).then(() => { - debug("set_volume: Succeeded."); - req.send_complete("Success"); - }).catch((error) => { - debug("set_volume: Failed with error."); + denon.client + .setVolume(newvol + 80) + .then(() => { + debug("set_volume: Succeeded."); + req.send_complete("Success"); + }) + .catch((error) => { + debug("set_volume: Failed with error."); - console.log(error); - req.send_complete("Failed"); - }); + console.log(error); + req.send_complete("Failed"); + }); }, - set_mute: function(req, inAction) { + set_mute: function (req, inAction) { debug("set_mute: action=%s", inAction); const action = !this.state.is_muted ? "on" : "off"; - denon.client.setMute(action === "on" ? Denon.Options.MuteOptions.On : Denon.Options.MuteOptions.Off) + denon.client + .setMute( + action === "on" + ? Denon.Options.MuteOptions.On + : Denon.Options.MuteOptions.Off, + ) .then(() => { debug("set_mute: Succeeded."); req.send_complete("Success"); - }).catch((error) => { + }) + .catch((error) => { debug("set_mute: Failed."); console.log(error); req.send_complete("Failed"); }); - } + }, }; } - let result = denon.client.getVolume().then((val) => { - denon.volume_state.volume_value = val - 80; - return denon.client.getMaxVolume(); - }).then((val) => { - denon.volume_state.volume_max = val - 80; - return denon.client.getMute(); - }).then((val) => { - denon.volume_state.is_muted = (val === Denon.Options.MuteOptions.On); - if (denon.volume_control) { - denon.volume_control.update_state(denon.volume_state); - } else { - debug("Registering volume control extension"); - denon.volume_control = svc_volume_control.new_device(device); - } - }); + let result = denon.client + .getVolume() + .then((val) => { + denon.volume_state.volume_value = val - 80; + return denon.client.getMaxVolume(); + }) + .then((val) => { + denon.volume_state.volume_max = val - 80; + return denon.client.getMute(); + }) + .then((val) => { + denon.volume_state.is_muted = val === Denon.Options.MuteOptions.On; + if (denon.volume_control) { + denon.volume_control.update_state(denon.volume_state); + } else { + debug("Registering volume control extension"); + denon.volume_control = svc_volume_control.new_device(device); + } + }); return result; } function create_source_control(denon) { - debug("create_source_control: source_control=%o", denon.source_control) + debug("create_source_control: source_control=%o", denon.source_control); if (!denon.source_control) { denon.source_state = { display_name: "Main Zone", supports_standby: true, status: "", Power: "", - Input: "" + Input: "", }; var device = { state: denon.source_state, control_key: 2, - convenience_switch: function(req) { + convenience_switch: function (req) { if (denon.source_state.Power === "STANDBY") { - denon.client.setPower('ON'); + denon.client.setPower("ON"); } if (denon.source_state.Input == mysettings.setsource) { req.send_complete("Success"); } else { - denon.client.setInput(mysettings.setsource).then(() => { - req.send_complete("Success"); - }).catch((error) => { - debug("set_source: Failed with error."); - req.send_complete("Failed"); - }); + denon.client + .setInput(mysettings.setsource) + .then(() => { + req.send_complete("Success"); + }) + .catch((error) => { + debug("set_source: Failed with error."); + req.send_complete("Failed"); + }); } }, - standby: function(req) { + standby: function (req) { denon.client.getPower().then((val) => { - denon.client.setPower(val === 'STANDBY' ? "ON" : "STANDBY").then(() => { - req.send_complete("Success"); - }).catch((error) => { - debug("set_standby: Failed with error."); - - console.log(error); - req.send_complete("Failed"); - }); + denon.client + .setPower(val === "STANDBY" ? "ON" : "STANDBY") + .then(() => { + req.send_complete("Success"); + }) + .catch((error) => { + debug("set_standby: Failed with error."); + + console.log(error); + req.send_complete("Failed"); + }); }); - } + }, }; } - let result = denon.client.getPower().then((val) => { - denon.source_state.Power = val; - return denon.client.getInput(); - }).then((val) => { - denon.source_state.Input = val; - denon.source_state.status = check_status(denon.source_state.Power, denon.source_state.Input); - if (denon.source_control) { - denon.source_control.update_state(denon.source_state); - } else { - debug("Registering source control extension"); - denon.source_control = svc_source_control.new_device(device); - } - }); + let result = denon.client + .getPower() + .then((val) => { + denon.source_state.Power = val; + return denon.client.getInput(); + }) + .then((val) => { + denon.source_state.Input = val; + denon.source_state.status = check_status( + denon.source_state.Power, + denon.source_state.Input, + ); + if (denon.source_control) { + denon.source_control.update_state(denon.source_state); + } else { + debug("Registering source control extension"); + denon.source_control = svc_source_control.new_device(device); + } + }); return result; } From 479eccce59ba2f0e1bae95992c0f3b0e0cd463f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Pr=C3=BC=C3=9Fmann?= Date: Fri, 17 Jan 2025 09:49:59 +0100 Subject: [PATCH 10/12] added Dockerfile --- Dockerfile | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0694b72 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM node:23-alpine3.21 + +# Create app directory +WORKDIR /usr/src/app + +# Install app dependencies +# A wildcard is used to ensure both package.json AND package-lock.json are copied +# where available (npm@5+) +COPY package*.json ./ + +RUN npm install +# If you are building your code for production +# RUN npm ci --only=production + +# Bundle app source +COPY . . + +CMD [ "node", "app.js" ] From 9538fb46e02dfe1856fd07c536d05d1f8a66c1e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Pr=C3=BC=C3=9Fmann?= Date: Fri, 17 Jan 2025 10:47:36 +0100 Subject: [PATCH 11/12] add information required for extension manager --- .reg/settings | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 .reg/settings diff --git a/.reg/settings b/.reg/settings new file mode 100644 index 0000000..d012cbe --- /dev/null +++ b/.reg/settings @@ -0,0 +1,30 @@ +# Set the AUTHOR variable to the name you want to be presented with to the user +AUTHOR="doc bobo" + +# Set the extension ID to a string that uniquely identifies your extension, e.g. based on an internet domain you own +EXTENSION_ID=org.pruessmann.roon-extension-denon + +# Set the NAME variable to the name of your extension +NAME=roon-extension-denon + +# Set the FRIENDLY_NAME variable to a more readable name for your extension +FRIENDLY_NAME="Denon/Marantz AVR" + +# Set the DESCRIPTION variable to a single line string that summarizes the function of your extension +DESCRIPTION="Roon Volume Control Extension for Denon/Marantz AVR receivers" + +# Set the VERSION variable to the version number of your extension +VERSION=2025.1.1 + +# Set the EMAIL variable to the address that users can use for support questions +EMAIL=docbobo@pm.me + +# Set the WEBSITE variable to the location where more information about your extension can be found +# This can be your website, a Roon Community forum post, the GitHub respository of your extension, etc. +WEBSITE=https://github.com/docbobo/roon-extension-denon + +# Set the MAIN_FILE variable to the main source file of your extension +MAIN_FILE=app.js + +# Set the USER variable to your Docker Hub account name +USER=docbobo From b4974885eb5b59564d8376c042ebef1b5da143bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Boris=20Pr=C3=BC=C3=9Fmann?= Date: Fri, 17 Jan 2025 11:32:36 +0100 Subject: [PATCH 12/12] start config of GH actions --- .github/workflows/deploy.yaml | 55 +++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 .github/workflows/deploy.yaml diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml new file mode 100644 index 0000000..88c47f8 --- /dev/null +++ b/.github/workflows/deploy.yaml @@ -0,0 +1,55 @@ +# +name: Create and publish a Docker image + +# Configures this workflow to run every time a change is pushed to the branch called `release`. +on: + push: + branches: ["master", "main"] + +# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds. +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu. +jobs: + build-and-push-image: + runs-on: ubuntu-latest + strategy: + matrix: + platform: [linux/amd64, linux/arm64, linux/arm] + + # Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job. + permissions: + contents: read + packages: write + # + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here. + - name: Log in to the Container registry + uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels. + - name: Extract metadata (tags, labels) for Docker + id: meta + uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + + # This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages. + # It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository. + # It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step. + - name: Build and push Docker image + uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }}