From 5726ec234ca59d7fddd3c5ffb665554f851efc64 Mon Sep 17 00:00:00 2001 From: marcus Date: Fri, 30 Sep 2016 17:42:00 +0200 Subject: [PATCH] Improved volume control mapping between absolute, dB, and slider levels, Added proper removal of event listeners to device destroy() methods to avoid potential resource leak, Cleanup --- denon-avr.coffee | 14 ------ devices/denon-avr-input-selector.coffee | 14 +++--- devices/denon-avr-master-volume.coffee | 14 +++--- devices/denon-avr-mute-switch.coffee | 19 +++++--- devices/denon-avr-power-switch.coffee | 14 +++--- devices/denon-avr-presence-sensor.coffee | 18 ++++---- devices/denon-avr-zone-switch.coffee | 14 +++--- devices/denon-avr-zone-volume.coffee | 14 +++--- http-app-protocol.coffee | 58 ++++++++++++------------ 9 files changed, 90 insertions(+), 89 deletions(-) diff --git a/denon-avr.coffee b/denon-avr.coffee index c72f1f2..a61bc59 100644 --- a/denon-avr.coffee +++ b/denon-avr.coffee @@ -38,20 +38,6 @@ module.exports = (env) -> "class": "DenonAvrInputSelector", } ] - commands = - POWER: /^(PW)([A-Z]+)/ - VOLUME: /^(MV)([0-9]+)/ - MAINMUTE: /^(MU)([A-Z]+)/ - Z2MUTE: /^(Z2MU)([A-Z]+)/ - Z3MUTE: /^(Z3MU)([A-Z]+)/ - INPUT: /^(SI)(.+)/ - MAIN: /^(ZM)(.+)/ - ZONE2: /^(Z2)(.+)/ - ZONE3: /^(Z3)(.+)/ - - settled = (promise) -> promise.reflect() - series = (input, mapper) -> Promise.mapSeries(input, mapper) - # ###DenonAvrPlugin class class DenonAvrPlugin extends env.plugins.Plugin diff --git a/devices/denon-avr-input-selector.coffee b/devices/denon-avr-input-selector.coffee index 4a18e58..de6b387 100644 --- a/devices/denon-avr-input-selector.coffee +++ b/devices/denon-avr-input-selector.coffee @@ -27,25 +27,27 @@ module.exports = (env) -> @debug = @plugin.debug || false for b in @config.buttons b.text = b.id unless b.text? - @plugin.protocolHandler.on 'response', @_onResponseHandler() + @responseHandler = @_createResponseHandler() + @plugin.protocolHandler.on 'response', @responseHandler super(@config) process.nextTick () => - @_requestUpdate() + @_requestUpdate true destroy: () -> @_base.cancelUpdate() + @plugin.protocolHandler.removeListener 'response', @responseHandler super() - _requestUpdate: () -> + _requestUpdate: (immediate=false) -> @_base.cancelUpdate() @_base.debug "Requesting update" - @plugin.protocolHandler.sendRequest @zoneCmd, '?' + @plugin.protocolHandler.sendRequest @zoneCmd, '?', immediate .catch (error) => @_base.error "Error:", error .finally () => - @_base.scheduleUpdate @_requestUpdate, @interval * 1000 + @_base.scheduleUpdate @_requestUpdate, @interval * 1000, true - _onResponseHandler: () -> + _createResponseHandler: () -> return (response) => @_base.debug "Response", response.matchedResults if response.command is @zoneCmd and response.param isnt @_lastPressedButton and response.param isnt 'OFF' diff --git a/devices/denon-avr-master-volume.coffee b/devices/denon-avr-master-volume.coffee index d6bb315..b905c49 100644 --- a/devices/denon-avr-master-volume.coffee +++ b/devices/denon-avr-master-volume.coffee @@ -20,7 +20,8 @@ module.exports = (env) -> @volumeLimit = @_base.normalize @config.volumeLimit, 0, 99 @maxAbsoluteVolume = @_base.normalize @config.maxAbsoluteVolume, 0, 99 @debug = @plugin.debug || false - @plugin.protocolHandler.on 'response', @_onResponseHandler() + @responseHandler = @_createResponseHandler() + @plugin.protocolHandler.on 'response', @responseHandler @attributes = _.cloneDeep(@attributes) @attributes.volume = { description: "Volume" @@ -33,22 +34,23 @@ module.exports = (env) -> @_volume = 0 super() process.nextTick () => - @_requestUpdate() + @_requestUpdate true destroy: () -> @_base.cancelUpdate() + @plugin.protocolHandler.removeListener 'response', @responseHandler super() - _requestUpdate: () -> + _requestUpdate: (immediate=false) -> @_base.cancelUpdate() @_base.debug "Requesting update" - @plugin.protocolHandler.sendRequest 'MV', '?' + @plugin.protocolHandler.sendRequest 'MV', '?', immediate .catch (error) => @_base.error "Error:", error .finally () => - @_base.scheduleUpdate(@_requestUpdate, @interval * 1000) + @_base.scheduleUpdate @_requestUpdate, @interval * 1000, true - _onResponseHandler: () -> + _createResponseHandler: () -> return (response) => @_base.debug "Response", response.matchedResults if response.command is 'MV' diff --git a/devices/denon-avr-mute-switch.coffee b/devices/denon-avr-mute-switch.coffee index ee028a4..f4c60c9 100644 --- a/devices/denon-avr-mute-switch.coffee +++ b/devices/denon-avr-mute-switch.coffee @@ -23,28 +23,31 @@ module.exports = (env) -> when 'ZONE3' then ( @zoneCmd = 'Z3MU' ) + @lastPowerState=null @interval = @_base.normalize @config.interval, 10 @debug = @plugin.debug || false - @plugin.protocolHandler.on 'response', @_onResponseHandler() + @responseHandler = @_createResponseHandler() + @plugin.protocolHandler.on 'response', @responseHandler super() @_state = false; process.nextTick () => - @_requestUpdate() + @_requestUpdate true destroy: () -> @_base.cancelUpdate() + @plugin.protocolHandler.removeListener 'response', @responseHandler super() - _requestUpdate: () -> + _requestUpdate: (immediate=false) -> @_base.cancelUpdate() @_base.debug "Requesting update" - @plugin.protocolHandler.sendRequest @zoneCmd, '?' + @plugin.protocolHandler.sendRequest @zoneCmd, '?', immediate .catch (error) => @_base.error "Error:", error .finally () => - @_base.scheduleUpdate @_requestUpdate, @interval * 1000 + @_base.scheduleUpdate @_requestUpdate, @interval * 1000, true - _onResponseHandler: () -> + _createResponseHandler: () -> return (response) => @_base.debug "Response", response.matchedResults @@ -53,7 +56,9 @@ module.exports = (env) -> @_setState if response.param is 'ON' then true else false ) when 'PW' then ( - @_requestUpdate() + powerState = response.param is 'ON' + @_requestUpdate() if powerState isnt @lastPowerState + @lastPowerState = powerState ) changeStateTo: (newState) -> diff --git a/devices/denon-avr-power-switch.coffee b/devices/denon-avr-power-switch.coffee index a8164e1..dba5d35 100644 --- a/devices/denon-avr-power-switch.coffee +++ b/devices/denon-avr-power-switch.coffee @@ -17,26 +17,28 @@ module.exports = (env) -> @name = @config.name @interval = @_base.normalize @config.interval, 10 @debug = @plugin.debug || false - @plugin.protocolHandler.on 'response', @_onResponseHandler() + @responseHandler = @_createResponseHandler() + @plugin.protocolHandler.on 'response', @responseHandler @_state = false super() process.nextTick () => - @_requestUpdate() + @_requestUpdate true destroy: () -> @_base.cancelUpdate() + @plugin.protocolHandler.removeListener 'response', @responseHandler super() - _requestUpdate: () -> + _requestUpdate: (immediate=false) -> @_base.cancelUpdate() @_base.debug "Requesting update" - @plugin.protocolHandler.sendRequest 'PW', '?' + @plugin.protocolHandler.sendRequest 'PW', '?', immediate .catch (error) => @_base.error "Error:", error .finally () => - @_base.scheduleUpdate @_requestUpdate, @interval * 1000 + @_base.scheduleUpdate @_requestUpdate, @interval * 1000, true - _onResponseHandler: () -> + _createResponseHandler: () -> return (response) => @_base.debug "Response", response.matchedResults if response.command is 'PW' diff --git a/devices/denon-avr-presence-sensor.coffee b/devices/denon-avr-presence-sensor.coffee index 6e1d20b..4a1b923 100644 --- a/devices/denon-avr-presence-sensor.coffee +++ b/devices/denon-avr-presence-sensor.coffee @@ -19,7 +19,8 @@ module.exports = (env) -> @interval = @_base.normalize @config.interval, 10 @volumeDecibel = @config.volumeDecibel @debug = @plugin.debug || false - @plugin.protocolHandler.on 'response', @_onResponseHandler() + @responseHandler = @_createResponseHandler() + @plugin.protocolHandler.on 'response', @responseHandler @attributes = _.cloneDeep(@attributes) @attributes.volume = { description: "Volume" @@ -37,26 +38,27 @@ module.exports = (env) -> @_input = "" super() process.nextTick () => - @_requestUpdate() + @_requestUpdate true destroy: () -> @_base.cancelUpdate() + @plugin.protocolHandler.removeListener 'response', @responseHandler super() - _requestUpdate: () -> + _requestUpdate: (immediate=false) -> @_base.cancelUpdate() @_base.debug "Requesting update" Promise.all([ - @plugin.protocolHandler.sendRequest 'PW', '?' - @plugin.protocolHandler.sendRequest 'SI', '?' - @plugin.protocolHandler.sendRequest 'MV', '?' + @plugin.protocolHandler.sendRequest 'PW', '?', immediate + @plugin.protocolHandler.sendRequest 'SI', '?', immediate + @plugin.protocolHandler.sendRequest 'MV', '?', immediate ]) .catch (error) => @_base.error "Error:", error .finally () => - @_base.scheduleUpdate @_requestUpdate, @interval * 1000 + @_base.scheduleUpdate @_requestUpdate, @interval * 1000, true - _onResponseHandler: () -> + _createResponseHandler: () -> return (response) => @_base.debug "Response", response.matchedResults switch response.command diff --git a/devices/denon-avr-zone-switch.coffee b/devices/denon-avr-zone-switch.coffee index 772efb0..a5da6e0 100644 --- a/devices/denon-avr-zone-switch.coffee +++ b/devices/denon-avr-zone-switch.coffee @@ -25,26 +25,28 @@ module.exports = (env) -> ) @interval = @_base.normalize @config.interval, 10 @debug = @plugin.debug || false - @plugin.protocolHandler.on 'response', @_onResponseHandler() + @responseHandler = @_createResponseHandler() + @plugin.protocolHandler.on 'response', @responseHandler @_state = false super() process.nextTick () => - @_requestUpdate() + @_requestUpdate true destroy: () -> @_base.cancelUpdate() + @plugin.protocolHandler.removeListener 'response', @responseHandler super() - _requestUpdate: () -> + _requestUpdate: (immediate=false) -> @_base.cancelUpdate() @_base.debug "Requesting update" - @plugin.protocolHandler.sendRequest @zoneCmd, '?' + @plugin.protocolHandler.sendRequest @zoneCmd, '?', immediate .catch (error) => @_base.error "Error:", error .finally () => - @_base.scheduleUpdate @_requestUpdate, @interval * 1000 + @_base.scheduleUpdate @_requestUpdate, @interval * 1000, true - _onResponseHandler: () -> + _createResponseHandler: () -> return (response) => @_base.debug "Response", response.matchedResults if response.command is @zoneCmd diff --git a/devices/denon-avr-zone-volume.coffee b/devices/denon-avr-zone-volume.coffee index 450104c..843ac81 100644 --- a/devices/denon-avr-zone-volume.coffee +++ b/devices/denon-avr-zone-volume.coffee @@ -28,7 +28,8 @@ module.exports = (env) -> @volumeLimit = @_base.normalize @config.volumeLimit, 0, 99 @maxAbsoluteVolume = @_base.normalize @config.maxAbsoluteVolume, 0, 99 @debug = @plugin.debug || false - @plugin.protocolHandler.on 'response', @_onResponseHandler() + @responseHandler = @_createResponseHandler() + @plugin.protocolHandler.on 'response', @responseHandler @attributes = _.cloneDeep(@attributes) @attributes.volume = { description: "Volume" @@ -41,22 +42,23 @@ module.exports = (env) -> @_volume = 0 super() process.nextTick () => - @_requestUpdate() + @_requestUpdate true destroy: () -> @_base.cancelUpdate() + @plugin.protocolHandler.removeListener 'response', @responseHandler super() - _requestUpdate: () -> + _requestUpdate: (immediate=false) -> @_base.cancelUpdate() @_base.debug "Requesting update" - @plugin.protocolHandler.sendRequest @zoneCmd, '?' + @plugin.protocolHandler.sendRequest @zoneCmd, '?', immediate .catch (error) => @_base.error "Error:", error .finally () => - @_base.scheduleUpdate(@_requestUpdate, @interval * 1000) + @_base.scheduleUpdate @_requestUpdate, @interval * 1000, true - _onResponseHandler: () -> + _createResponseHandler: () -> return (response) => @_base.debug "Response", response.matchedResults if response.command is @zoneCmd and not isNaN response.param diff --git a/http-app-protocol.coffee b/http-app-protocol.coffee index 7302d58..199b1b3 100644 --- a/http-app-protocol.coffee +++ b/http-app-protocol.coffee @@ -7,21 +7,6 @@ module.exports = (env) -> retry = require('promise-retryer')(Promise) commons = require('pimatic-plugin-commons')(env) - commands = - POWER: /^(PW)([A-Z]+)/ - VOLUME: /^(MV)([0-9]+)/ - MAINMUTE: /^(MU)([A-Z]+)/ - Z2MUTE: /^(Z2MU)([A-Z]+)/ - Z3MUTE: /^(Z3MU)([A-Z]+)/ - INPUT: /^(SI)(.+)/ - MAIN: /^(ZM)(.+)/ - ZONE2: /^(Z2)(.+)/ - ZONE3: /^(Z3)(.+)/ - - settled = (promise) -> promise.reflect() - series = (input, mapper) -> Promise.mapSeries(input, mapper) - - # ###DenonAvrPlugin class class HttpAppProtocol extends require('events').EventEmitter constructor: (@config) -> @@ -30,6 +15,8 @@ module.exports = (env) -> @port = @config.port || 80 @debug = @config.debug || false @base = commons.base @, 'HttpAppProtocol' + @on "newListener", => + @base.debug "Status response event listeners: #{1 + @listenerCount 'response'}" pause: (ms=50) -> @base.debug "Pausing:", ms, "ms" @@ -41,19 +28,20 @@ module.exports = (env) -> when 'Z3' then return 'formZone3_Zone3' else return 'formMainZone_MainZone' - _mapZoneToPrefix: (command) -> + _mapZoneToCommandPrefix: (command) -> switch command[...2] when 'Z2' then return 'Z2' when 'Z3' then return 'Z3' else return '' - _mapZoneToKey: (command) -> + _mapZoneToObjectKey: (command) -> switch command[...2] when 'Z2' then return 'zone2' when 'Z3' then return 'zone3' else return 'main' _triggerResponse: (command, param) -> + # emulate the regex matcher of telnet transport - should be refactored @emit 'response', matchedResults: [ "#{command}#{param}" @@ -71,26 +59,36 @@ module.exports = (env) -> return rest.get "http://#{@host}:#{@port}/goform/#{@_mapZoneToUrlPath command}XmlStatusLite.xml" .then (response) => if response.data.length isnt 0 + @base.debug response.data parseXmlString response.data .then (dom) => - delete @scheduledUpdates[@_mapZoneToKey command] if @scheduledUpdates[@_mapZoneToKey command]? - prefix = @_mapZoneToPrefix command + prefix = @_mapZoneToCommandPrefix command @_triggerResponse "#{prefix}MU", dom.item.Mute[0].value[0].toUpperCase() @_triggerResponse "#{prefix}PW", dom.item.Power[0].value[0].toUpperCase() - volume = parseInt dom.item.MasterVolume[0].value[0], 10 - if dom.item.VolumeDisplay[0].value[0].toUpperCase() is 'ABSOLUTE' - volume += 80 - @_triggerResponse "#{prefix}MV", volume + volume = parseInt(dom.item.MasterVolume[0].value[0], 10) + if not isNaN volume + if dom.item.VolumeDisplay[0].value[0].toUpperCase() is 'ABSOLUTE' + volume += 80 + @_triggerResponse "#{prefix}MV", volume @_triggerResponse "#{prefix}SI", dom.item.InputFuncSelect[0].value[0].toUpperCase() + else + throw new Error "Empty result received for status request" + .finally => + delete @scheduledUpdates[@_mapZoneToObjectKey command] if @scheduledUpdates[@_mapZoneToObjectKey command]? - _scheduleUpdate: (command, param="") -> - if not @scheduledUpdates[@_mapZoneToKey command]? - @base.debug "Scheduling update for zone #{@_mapZoneToKey command}" - @scheduledUpdates[@_mapZoneToKey command] = true - @base.scheduleUpdate @_requestUpdate, 1000, command, param + _scheduleUpdate: (command, param="", immediate) -> + timeout=1500 + if not @scheduledUpdates[@_mapZoneToObjectKey command]? + @base.debug "Scheduling update for zone #{@_mapZoneToObjectKey command}" + @scheduledUpdates[@_mapZoneToObjectKey command] = true + timeout=0 if immediate + else + @base.debug "Re-scheduling update for zone #{@_mapZoneToObjectKey command}" + @base.cancelUpdate() + @base.scheduleUpdate @_requestUpdate, timeout, command, param return Promise.resolve() - sendRequest: (command, param="") -> + sendRequest: (command, param="", immediate=false) -> return new Promise (resolve, reject) => if param isnt '?' @base.debug "http://#{@host}:#{@port}/goform/formiPhoneAppDirect.xml?#{command}#{param}" @@ -98,7 +96,7 @@ module.exports = (env) -> .then => @_triggerResponse command, param else - promise = @_scheduleUpdate command, param + promise = @_scheduleUpdate command, param, immediate promise.then => resolve()