From e150e6ece704d3d07e36b8f86d90a4b4c57d1b2c Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 16 May 2022 17:10:37 +0100 Subject: [PATCH 1/3] Update for Indigo 2022.1 Convert to Python 3 and update to latest RXV library (V 7.0) --- .DS_Store | Bin 0 -> 6148 bytes Yahama RX.indigoPlugin/Contents/Info.plist | 4 +- .../Contents/Server Plugin/__init__.py | 0 .../Contents/Server Plugin/plugin.py | 132 ++--- .../Contents/Server Plugin/rxv/.gitignore | 1 + .../Contents/Server Plugin/rxv/LICENSE | 10 - .../Contents/Server Plugin/rxv/__init__.py | 16 +- .../rxv/defusedxml/ElementTree.py | 154 ++++++ .../Server Plugin/rxv/defusedxml/__init__.py | 67 +++ .../rxv/defusedxml/cElementTree.py | 62 +++ .../Server Plugin/rxv/defusedxml/common.py | 129 +++++ .../rxv/defusedxml/expatbuilder.py | 107 ++++ .../rxv/defusedxml/expatreader.py | 61 +++ .../Server Plugin/rxv/defusedxml/lxml.py | 153 ++++++ .../Server Plugin/rxv/defusedxml/minidom.py | 63 +++ .../Server Plugin/rxv/defusedxml/pulldom.py | 41 ++ .../Server Plugin/rxv/defusedxml/sax.py | 60 +++ .../Server Plugin/rxv/defusedxml/xmlrpc.py | 153 ++++++ .../Contents/Server Plugin/rxv/exceptions.py | 21 +- .../Contents/Server Plugin/rxv/rxv.py | 504 ++++++++++++++++-- .../Contents/Server Plugin/rxv/ssdp.py | 29 +- Yahama RX.indigoPlugin/Contents/__init__.py | 0 Yahama RX.indigoPlugin/__init__.py | 0 23 files changed, 1624 insertions(+), 143 deletions(-) create mode 100644 .DS_Store create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/__init__.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/.gitignore delete mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/LICENSE create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/ElementTree.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/__init__.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/cElementTree.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatbuilder.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatreader.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/lxml.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/minidom.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/pulldom.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/sax.py create mode 100644 Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/xmlrpc.py create mode 100644 Yahama RX.indigoPlugin/Contents/__init__.py create mode 100644 Yahama RX.indigoPlugin/__init__.py diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..c783ce4d678ab90fe483a52d3cf841f579c61263 GIT binary patch literal 6148 zcmeHKF;2r!47F*7NGu&;`#Ta+Z}3}%6I6&5l}ZJvRWiVyjcafi4#3V0I0eu42PKNi z$N2jv54sEX}b`ai%1POlxJI}W_k08os>ugRJ+H1w^-k`hi+1NJYd{P zE@dYV@;Kt_$Hg#rXq#rUZ#Rh5H{JW&_4(^^oZtSK@9qcBHhTe;Mg^z<6`%rC;9n|$ zo^4j226CkWRDcS6D`4M;1UJkD`#}G6VDS+EXut7pxb|5Bm@ELy1^YlmU>a0lP_>L0 z8g#@{smlfXz@Urz$+%BBS<8f?emdgG%SCe_S1LdS#tO`0JGcIS1wS(Xk4fB70V?pP z6ws8+EOvtFCvx9~4Rt(Rl*Rt)r3jE%M8vq4?5YvgmmKG5liI~~X$0n>#> I1%5+;Pf9^4eE PluginVersion - 1.0.4 + 2022.0.0 ServerApiVersion - 2.0 + 3.0 IwsApiVersion 1.0.0 CFBundleDisplayName diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/__init__.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py index 2b88895..5dd759f 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py @@ -1,10 +1,10 @@ #! /usr/bin/env python # -*- coding: utf-8 -*- -import urllib2 +import urllib.request, urllib.error, urllib.parse import traceback -import requests +# import requests from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout import rxv @@ -17,8 +17,10 @@ from xml.etree.ElementTree import ParseError -try: import indigo -except: pass +try: + import indigo +except: + pass kSleepBetweenUpdatePolls = 2 # number of seconds to sleep between polling each receiver for current status kSleepValueMap = { @@ -32,6 +34,7 @@ def str2bool(v): return v.lower() in ("yes", "true", "t", "1", "on") + class ClassicReceiver(object): kInputList = ( ('tuner', 'TUNER'), @@ -55,11 +58,11 @@ class ClassicReceiver(object): def xmitToReceiver(dev, xml_string): url = 'http://' + dev.pluginProps['txtip'] + '/YamahaRemoteControl/ctrl' - req = urllib2.Request( + req = urllib.request.Request( url=url, data=xml_string, headers={'Content-Type': 'application/xml'}) - resp = urllib2.urlopen(req) + resp = urllib.request.urlopen(req) status_xml = resp.read() root = ET.fromstring(status_xml) return root @@ -67,11 +70,11 @@ def xmitToReceiver(dev, xml_string): @staticmethod def putMute(logger, dev, val): if dev is None: - logger.debug(u"no device defined") + logger.debug("no device defined") return if val is None: - logger.debug(u"value not defined") + logger.debug("value not defined") return xml_string = ''+val+'' @@ -80,11 +83,11 @@ def putMute(logger, dev, val): @staticmethod def putVolume(logger, dev, val): if dev is None: - logger.debug(u"no device defined") + logger.debug("no device defined") return if val is None: - logger.debug(u"value not defined") + logger.debug("value not defined") return xml_string = ''+val+'1dB' @@ -93,11 +96,11 @@ def putVolume(logger, dev, val): @staticmethod def putPower(logger, dev, val): if dev is None: - logger.debug(u"no device defined") + logger.debug("no device defined") return if val is None: - logger.debug(u"value not defined") + logger.debug("value not defined") return xml_string = ''+val+'' @@ -106,11 +109,11 @@ def putPower(logger, dev, val): @staticmethod def putSleep(logger, dev, val): if dev is None: - logger.debug(u"no device defined") + logger.debug("no device defined") return if val is None: - logger.debug(u"value not defined") + logger.debug("value not defined") return xml_string = ''+val+'' @@ -119,18 +122,17 @@ def putSleep(logger, dev, val): @staticmethod def putInput(logger, dev, val): if dev is None: - logger.debug(u"no device defined") + logger.debug("no device defined") return if val is None: - logger.debug(u"value not defined") + logger.debug("value not defined") return xml_string = ''+val+'' root = ClassicReceiver.xmitToReceiver( dev, xml_string) - class Plugin(indigo.PluginBase): def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): @@ -143,12 +145,12 @@ def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): # Standard plugin operation methods ################################## def startup(self): - self.logger.debug(u"startup called") + self.logger.debug("startup called") def shutdown(self): - self.logger.debug(u"shutdown called") + self.logger.debug("shutdown called") - def deviceStartComm(self, dev): + def device_start_comm(self, dev): if dev.deviceTypeId == "receiver": devTup = (dev,) elif dev.deviceTypeId == "rxvX73": @@ -160,9 +162,9 @@ def deviceStartComm(self, dev): except (ConnectTimeout, ReadTimeout, ConnectionError) as e: dev.setErrorStateOnServer('unavailable') if isinstance(e, ConnectTimeout) or isinstance(e, ReadTimeout): - self.logger.debug("device '%s' connection timed out" % dev.name) + self.logger.debug(f"device '{dev.name}' connection timed out") else: - self.logger.debug("device '%s' had a connection error" % dev.name) + self.logger.debug(f"device '{dev.name}' had a connection error") return except ParseError: # This seems to happen relatively frequently - apparently sometimes the amp goes out to lunch for @@ -171,28 +173,28 @@ def deviceStartComm(self, dev): dev.setErrorStateOnServer('unavailable') devTup = (dev, None) except Exception as e: - self.logger.error("Couldn't start device %s - it may not be available on the network or the IP address may have changed." % dev.name) - self.logger.debug("exception starting device:\n%s" % traceback.format_exc(10)) + self.logger.error(f"Couldn't start device {dev.name} - it may not be available on the network or the IP address may have changed.") + self.logger.debug(f"exception starting device:\n{traceback.format_exc(10)}") return self.devices[dev.id] = devTup self.updateStatus(dev.id) - def deviceStopComm(self, dev): + def device_stop_comm(self, dev): try: del self.devices[dev.id] except: pass - def runConcurrentThread(self): + def run_concurrent_thread(self): try: while True: - for devId in self.devices.keys(): + for devId in list(self.devices.keys()): self.updateStatus(devId) self.sleep(2) except self.StopThread: return except Exception as e: - self.logger.error("runConcurrentThread error: \n%s" % traceback.format_exc(10)) + self.logger.error(f"runConcurrentThread error: \n{traceback.format_exc(10)}") ################################## # Config dialog methods @@ -203,7 +205,7 @@ def runConcurrentThread(self): # Returns the list of receivers as a tuple for the device config dialog. ############### def get_receiver_list(self, filter="", valuesDict=None, typeId="", targetId=0): - return [(k, v.friendly_name) for k, v in self.receivers.iteritems()] + return [(k, v.friendly_name) for k, v in self.receivers.items()] def get_input_list(self, filter="", valuesDict=None, typeId="", targetId=0): dev = indigo.devices.get(targetId, None) @@ -212,7 +214,7 @@ def get_input_list(self, filter="", valuesDict=None, typeId="", targetId=0): elif dev.deviceTypeId == "rxvX73": dev, rxv_obj = self.devices.get(dev.id, (None, None)) if rxv_obj: - return [(k.replace("iPod (USB)", "iPod").replace(" ", "_"), k) for k in rxv_obj.inputs().keys()] + return [(k.replace("iPod (USB)", "iPod").replace(" ", "_"), k) for k in list(rxv_obj.inputs().keys())] return [] def get_zone_list(self, filter="", valuesDict=None, typeId="", targetId=0): @@ -225,7 +227,7 @@ def get_zone_list(self, filter="", valuesDict=None, typeId="", targetId=0): ######################################## # Prefs dialog methods ######################################## - def closedPrefsConfigUi(self, valuesDict, userCancelled): + def closed_prefs_config_ui(self, valuesDict, userCancelled): # Since the dialog closed we want to set the debug flag - if you don't directly use # a plugin's properties (and for debugLog we don't) you'll want to translate it to # the appropriate stuff here. @@ -250,14 +252,14 @@ def closedPrefsConfigUi(self, valuesDict, userCancelled): ############### def refresh_receiver_list(self, filter="", valuesDict=None, typeId="", targetId=0): self.receivers = {r.ctrl_url: r for r in rxv.find()} - self.logger.debug("receivers list: %s" % unicode(self.receivers)) + self.logger.debug(f"receivers list: {str(self.receivers)}") ############### # updateStatus # # Updates the status for the specified device. ############### - def updateStatus(self, dev_id): + def update_status(self, dev_id): devTup = self.devices.get(dev_id, None) if devTup: dev = devTup[0] @@ -302,20 +304,20 @@ def updateStatus(self, dev_id): dev.setErrorStateOnServer(None) except (ConnectTimeout, ReadTimeout, ConnectionError) as e: dev.setErrorStateOnServer('unavailable') - if isinstance(e, ConnectTimeout) or isinstance(e, ReadTimeout): - self.logger.debug("device '%s' connection timed out" % dev.name) + if isinstance(e, ConnectTimeout) or isinstance(e, ReadTimeout): + self.logger.debug(f"device '{dev.name}' connection timed out") else: - self.logger.debug("device '%s' had a connection error" % dev.name) + self.logger.debug(f"device '{dev.name}' had a connection error") except ParseError: # dev.setErrorStateOnServer('unavailable') - # self.logger.debug("device '%s' failed to update status with an XML parse error" % dev.name) + # self.logger.debug(f"device '{dev.name}' failed to update status with an XML parse error") # This seems to happen relatively frequently - apparently sometimes the amp goes out to lunch for # a bit (causing connection errors) and when it comes back it doesn't always return correct XML. # I think the better idea here is to just pass on these errors as it always seems to resolve itself. pass except Exception as e: dev.setErrorStateOnServer('unavailable') - self.logger.debug("device '%s' failed to update status with error: \n%s" % (dev.name, traceback.format_exc(10))) + self.logger.debug(f"device '{dev.name}' failed to update status with error: \n{traceback.format_exc(10)}") ############### # _set_rxv_property @@ -334,15 +336,15 @@ def _set_rxv_property(self, dev, property, value): else: setattr(rxv_obj, property, value) else: - self.logger.error("device '%s' isn't available" % dev.name) + self.logger.error(f"device '{dev.name}' isn't available") except (ConnectTimeout, ReadTimeout, ConnectionError) as e: dev.setErrorStateOnServer('unavailable') if isinstance(e, ConnectTimeout) or isinstance(e, ReadTimeout): - self.logger.debug("device '%s' connection timed out" % dev.name) - self.logger.error("device '%s' is unavailable" % dev.name) + self.logger.debug(f"device '{dev.name}' connection timed out") + self.logger.error(f"device '{dev.name}' is unavailable") else: - self.logger.debug("device '%s' had a connection error" % dev.name) - self.logger.error("device '%s' is unavailable" % dev.name) + self.logger.debug(f"device '{dev.name}' had a connection error") + self.logger.error(f"device '{dev.name}' is unavailable") except rxv_exceptions.ResponseException as e: response = ET.XML(str(e)) if response.get("RC") == "3": @@ -350,21 +352,21 @@ def _set_rxv_property(self, dev, property, value): # We just skip it. pass elif response.get("RC") != "4": - self.logger.error("device '%s' can't have property '%s' set to value '%s'" % (dev.name, property, str(value))) + self.logger.error(f"device '{dev.name}' can't have property '{property}' set to value '{str(value)}'") else: # RC 4 is what happens when the amp is offline or in standby and you try to send it a command other than # to turn on (if in standby) - self.logger.error("device '%s' is unavailable" % dev.name) + self.logger.error(f"device '{dev.name}' is unavailable") except ET.ParseError: # dev.setErrorStateOnServer('unavailable') - # self.logger.debug("device '%s' failed to update status with an XML parse error" % dev.name) + # self.logger.debug(f"device '{dev.name}' failed to update status with an XML parse error") # This seems to happen relatively frequently - apparently sometimes the amp goes out to lunch for # a bit (causing connection errors) and when it comes back it doesn't always return correct XML. # I think the better idea here is to just pass on these errors as it always seems to resolve itself. pass except: dev.setErrorStateOnServer('unavailable') - self.logger.error("device '%s' failed to set property status with error: \n%s" % (dev.name, traceback.format_exc(10))) + self.logger.error(f"device '{dev.name}' failed to set property status with error: \n{traceback.format_exc(10)}") ################################## # Action methods @@ -373,7 +375,7 @@ def getStatus(self, pluginAction, dev): self.updateStatus(dev.id) def setMute(self, pluginAction, dev): - self.logger.debug(u"setMute called") + self.logger.debug("setMute called") val = pluginAction.props['ddlmute'] if dev.deviceTypeId == "receiver": ClassicReceiver.putMute(self.logger, dev, val) @@ -381,7 +383,7 @@ def setMute(self, pluginAction, dev): self._set_rxv_property(dev, 'mute', str2bool(val)) def toggleMute(self, pluginAction, dev): - self.logger.debug(u"toggleMute called") + self.logger.debug("toggleMute called") self.updateStatus(dev.id) dev.refreshFromServer() if dev.deviceTypeId == "receiver": @@ -391,7 +393,7 @@ def toggleMute(self, pluginAction, dev): self._set_rxv_property(dev, 'mute', not str2bool(dev.states['mute'])) def setVolume(self, pluginAction, dev): - self.logger.debug(u"setVolume called") + self.logger.debug("setVolume called") val = pluginAction.props['txtvolume'] if dev.deviceTypeId == "receiver": ClassicReceiver.putVolume(self.logger, dev, val) @@ -399,7 +401,7 @@ def setVolume(self, pluginAction, dev): self._set_rxv_property(dev, 'volume', float(val)) def increaseVolume(self, pluginAction, dev): - self.logger.debug(u"increaseVolume called") + self.logger.debug("increaseVolume called") self.updateStatus(dev.id) dev.refreshFromServer() val = float(dev.states['volume']) + int(pluginAction.props['txtincrement']) @@ -409,7 +411,7 @@ def increaseVolume(self, pluginAction, dev): self._set_rxv_property(dev, 'volume', val) def decreaseVolume(self, pluginAction, dev): - self.logger.debug(u"decreaseVolume called") + self.logger.debug("decreaseVolume called") self.updateStatus(dev.id) dev.refreshFromServer() val = float(dev.states['volume']) - int(pluginAction.props['txtincrement']) @@ -419,7 +421,7 @@ def decreaseVolume(self, pluginAction, dev): self._set_rxv_property(dev, 'volume', val) def setPower(self, pluginAction, dev): - self.logger.debug(u"setPower called") + self.logger.debug("setPower called") val = pluginAction.props['ddlpower'] if dev.deviceTypeId == "receiver": ClassicReceiver.putPower(self.logger, dev, val) @@ -427,7 +429,7 @@ def setPower(self, pluginAction, dev): self._set_rxv_property(dev, 'on', str2bool(val)) def togglePower(self, pluginAction, dev): - self.logger.debug(u"togglePower called") + self.logger.debug("togglePower called") self.updateStatus(dev.id) dev.refreshFromServer() val = 'On' if (dev.states['power']=='Standby') else 'Standby' @@ -437,15 +439,15 @@ def togglePower(self, pluginAction, dev): self._set_rxv_property(dev, 'on', str2bool(val)) def setSleep(self, pluginAction, dev): - self.logger.debug(u"setSleep called") + self.logger.debug("setSleep called") val = pluginAction.props['ddlsleep'].replace('n','') if dev.deviceTypeId == "receiver": ClassicReceiver.putSleep(self.logger, dev, val) elif dev.deviceTypeId == "rxvX73": - self._set_rxv_property(dev, 'sleep', "Off" if val == "Off" else "%s min" % val) + self._set_rxv_property(dev, 'sleep', "Off" if val == "Off" else f"{val} min") def setInput(self, pluginAction, dev): - self.logger.debug(u"setInput called") + self.logger.debug("setInput called") if dev.deviceTypeId == "receiver": val = pluginAction.props['ddlinput'].upper().replace(".","/").replace("_"," ") ClassicReceiver.putInput(self.logger, dev, val) @@ -453,33 +455,33 @@ def setInput(self, pluginAction, dev): self._set_rxv_property(dev, 'input', pluginAction.props['ddlinput'].replace("_", " ").replace("iPod", "iPod (USB)")) def setZone(self, pluginAction, dev): - self.logger.debug(u"setZone called") + self.logger.debug("setZone called") self._set_rxv_property(dev, 'zone', pluginAction.props['zone']) def playNetRadio(self, pluginAction, dev): - self.logger.debug(u"playNetRadio called") + self.logger.debug("playNetRadio called") self._set_rxv_property(dev, 'net_radio', pluginAction.props['path']) def menuUp(self, pluginAction, dev): - self.logger.debug(u"menuUp called") + self.logger.debug("menuUp called") self._set_rxv_property(dev, 'menu_up', None) def menuDown(self, pluginAction, dev): - self.logger.debug(u"menuDown called") + self.logger.debug("menuDown called") self._set_rxv_property(dev, 'menu_down', None) def menuLeft(self, pluginAction, dev): - self.logger.debug(u"menuLeft called") + self.logger.debug("menuLeft called") self._set_rxv_property(dev, 'menu_left', None) def menuRight(self, pluginAction, dev): - self.logger.debug(u"menuRight called") + self.logger.debug("menuRight called") self._set_rxv_property(dev, 'menu_right', None) def menuSelect(self, pluginAction, dev): - self.logger.debug(u"menuSelect called") + self.logger.debug("menuSelect called") self._set_rxv_property(dev, 'menu_sel', None) def menuReturn(self, pluginAction, dev): - self.logger.debug(u"menuReturn called") + self.logger.debug("menuReturn called") self._set_rxv_property(dev, 'menu_return', None) \ No newline at end of file diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/.gitignore b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/.gitignore new file mode 100644 index 0000000..9852786 --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/.gitignore @@ -0,0 +1 @@ +version.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/LICENSE b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/LICENSE deleted file mode 100644 index 0925749..0000000 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/LICENSE +++ /dev/null @@ -1,10 +0,0 @@ -Copyright (c) 2013, Joanna Tustanowska & Wojciech Bederski -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - - - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - - Names of contributors may not be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/__init__.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/__init__.py index 2f4faeb..a6f11e3 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/__init__.py +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/__init__.py @@ -1,11 +1,11 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -from __future__ import division, absolute_import, print_function +from __future__ import absolute_import, division, print_function + import logging -from .rxv import RXV -from .rxv import PlaybackSupport from . import ssdp +from .rxv import RXV __all__ = ['RXV'] @@ -16,12 +16,4 @@ def find(timeout=1.5): """Find all Yamah receivers on local network using SSDP search.""" - return [ - RXV( - ctrl_url=ri.ctrl_url, - model_name=ri.model_name, - friendly_name=ri.friendly_name, - unit_desc_url=ri.unit_desc_url - ) - for ri in ssdp.discover(timeout=timeout) - ] + return [RXV(**ri._asdict()) for ri in ssdp.discover(timeout=timeout)] diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/ElementTree.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/ElementTree.py new file mode 100644 index 0000000..5ba765f --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/ElementTree.py @@ -0,0 +1,154 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Defused xml.etree.ElementTree facade +""" +from __future__ import print_function, absolute_import + +import sys +import warnings +from xml.etree.ElementTree import ParseError +from xml.etree.ElementTree import TreeBuilder as _TreeBuilder +from xml.etree.ElementTree import parse as _parse +from xml.etree.ElementTree import tostring + +from .common import PY3 + +if PY3: + import importlib +else: + from xml.etree.ElementTree import XMLParser as _XMLParser + from xml.etree.ElementTree import iterparse as _iterparse + + +from .common import ( + DTDForbidden, + EntitiesForbidden, + ExternalReferenceForbidden, + _generate_etree_functions, +) + +__origin__ = "xml.etree.ElementTree" + + +def _get_py3_cls(): + """Python 3.3 hides the pure Python code but defusedxml requires it. + + The code is based on test.support.import_fresh_module(). + """ + pymodname = "xml.etree.ElementTree" + cmodname = "_elementtree" + + pymod = sys.modules.pop(pymodname, None) + cmod = sys.modules.pop(cmodname, None) + + sys.modules[cmodname] = None + try: + pure_pymod = importlib.import_module(pymodname) + finally: + # restore module + sys.modules[pymodname] = pymod + if cmod is not None: + sys.modules[cmodname] = cmod + else: + sys.modules.pop(cmodname, None) + # restore attribute on original package + etree_pkg = sys.modules["xml.etree"] + if pymod is not None: + etree_pkg.ElementTree = pymod + elif hasattr(etree_pkg, "ElementTree"): + del etree_pkg.ElementTree + + _XMLParser = pure_pymod.XMLParser + _iterparse = pure_pymod.iterparse + # patch pure module to use ParseError from C extension + pure_pymod.ParseError = ParseError + + return _XMLParser, _iterparse + + +if PY3: + _XMLParser, _iterparse = _get_py3_cls() + + +_sentinel = object() + + +class DefusedXMLParser(_XMLParser): + def __init__( + self, + html=_sentinel, + target=None, + encoding=None, + forbid_dtd=False, + forbid_entities=True, + forbid_external=True, + ): + # Python 2.x old style class + _XMLParser.__init__(self, target=target, encoding=encoding) + if html is not _sentinel: + # the 'html' argument has been deprecated and ignored in all + # supported versions of Python. Python 3.8 finally removed it. + if html: + raise TypeError("'html=True' is no longer supported.") + else: + warnings.warn( + "'html' keyword argument is no longer supported. Pass " + "in arguments as keyword arguments.", + category=DeprecationWarning, + ) + + self.forbid_dtd = forbid_dtd + self.forbid_entities = forbid_entities + self.forbid_external = forbid_external + if PY3: + parser = self.parser + else: + parser = self._parser + if self.forbid_dtd: + parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl + if self.forbid_entities: + parser.EntityDeclHandler = self.defused_entity_decl + parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl + if self.forbid_external: + parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler + + def defused_start_doctype_decl(self, name, sysid, pubid, has_internal_subset): + raise DTDForbidden(name, sysid, pubid) + + def defused_entity_decl( + self, name, is_parameter_entity, value, base, sysid, pubid, notation_name + ): + raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name) + + def defused_unparsed_entity_decl(self, name, base, sysid, pubid, notation_name): + # expat 1.2 + raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) # pragma: no cover + + def defused_external_entity_ref_handler(self, context, base, sysid, pubid): + raise ExternalReferenceForbidden(context, base, sysid, pubid) + + +# aliases +# XMLParse is a typo, keep it for backwards compatibility +XMLTreeBuilder = XMLParse = XMLParser = DefusedXMLParser + +parse, iterparse, fromstring = _generate_etree_functions( + DefusedXMLParser, _TreeBuilder, _parse, _iterparse +) +XML = fromstring + + +__all__ = [ + "ParseError", + "XML", + "XMLParse", + "XMLParser", + "XMLTreeBuilder", + "fromstring", + "iterparse", + "parse", + "tostring", +] diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/__init__.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/__init__.py new file mode 100644 index 0000000..4b5a230 --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/__init__.py @@ -0,0 +1,67 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Defuse XML bomb denial of service vulnerabilities +""" +from __future__ import print_function, absolute_import + +import warnings + +from .common import ( + DefusedXmlException, + DTDForbidden, + EntitiesForbidden, + ExternalReferenceForbidden, + NotSupportedError, + _apply_defusing, +) + + +def defuse_stdlib(): + """Monkey patch and defuse all stdlib packages + + :warning: The monkey patch is an EXPERIMETNAL feature. + """ + defused = {} + + with warnings.catch_warnings(): + from . import cElementTree + from . import ElementTree + from . import minidom + from . import pulldom + from . import sax + from . import expatbuilder + from . import expatreader + from . import xmlrpc + + xmlrpc.monkey_patch() + defused[xmlrpc] = None + + defused_mods = [ + cElementTree, + ElementTree, + minidom, + pulldom, + sax, + expatbuilder, + expatreader, + ] + + for defused_mod in defused_mods: + stdlib_mod = _apply_defusing(defused_mod) + defused[defused_mod] = stdlib_mod + + return defused + + +__version__ = "0.7.1" + +__all__ = [ + "DefusedXmlException", + "DTDForbidden", + "EntitiesForbidden", + "ExternalReferenceForbidden", + "NotSupportedError", +] diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/cElementTree.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/cElementTree.py new file mode 100644 index 0000000..84670c6 --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/cElementTree.py @@ -0,0 +1,62 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Defused xml.etree.cElementTree +""" +from __future__ import absolute_import + +import warnings + +from .common import _generate_etree_functions + +from xml.etree.cElementTree import TreeBuilder as _TreeBuilder +from xml.etree.cElementTree import parse as _parse +from xml.etree.cElementTree import tostring + +# iterparse from ElementTree! +from xml.etree.ElementTree import iterparse as _iterparse + +# This module is an alias for ElementTree just like xml.etree.cElementTree +from .ElementTree import ( + XML, + XMLParse, + XMLParser, + XMLTreeBuilder, + fromstring, + iterparse, + parse, + tostring, + DefusedXMLParser, + ParseError, +) + +__origin__ = "xml.etree.cElementTree" + + +warnings.warn( + "defusedxml.cElementTree is deprecated, import from defusedxml.ElementTree instead.", + category=DeprecationWarning, + stacklevel=2, +) + +# XMLParse is a typo, keep it for backwards compatibility +XMLTreeBuilder = XMLParse = XMLParser = DefusedXMLParser + +parse, iterparse, fromstring = _generate_etree_functions( + DefusedXMLParser, _TreeBuilder, _parse, _iterparse +) +XML = fromstring + +__all__ = [ + "ParseError", + "XML", + "XMLParse", + "XMLParser", + "XMLTreeBuilder", + "fromstring", + "iterparse", + "parse", + "tostring", +] diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py new file mode 100644 index 0000000..5ceda1f --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py @@ -0,0 +1,129 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Common constants, exceptions and helpe functions +""" +import sys +import xml.parsers.expat + +PY3 = sys.version_info[0] == 3 + +# Fail early when pyexpat is not installed correctly +if not hasattr(xml.parsers.expat, "ParserCreate"): + raise ImportError("pyexpat") # pragma: no cover + + +class DefusedXmlException(ValueError): + """Base exception""" + + def __repr__(self): + return str(self) + + +class DTDForbidden(DefusedXmlException): + """Document type definition is forbidden""" + + def __init__(self, name, sysid, pubid): + super(DTDForbidden, self).__init__() + self.name = name + self.sysid = sysid + self.pubid = pubid + + def __str__(self): + tpl = "DTDForbidden(name='{}', system_id={!r}, public_id={!r})" + return tpl.format(self.name, self.sysid, self.pubid) + + +class EntitiesForbidden(DefusedXmlException): + """Entity definition is forbidden""" + + def __init__(self, name, value, base, sysid, pubid, notation_name): + super(EntitiesForbidden, self).__init__() + self.name = name + self.value = value + self.base = base + self.sysid = sysid + self.pubid = pubid + self.notation_name = notation_name + + def __str__(self): + tpl = "EntitiesForbidden(name='{}', system_id={!r}, public_id={!r})" + return tpl.format(self.name, self.sysid, self.pubid) + + +class ExternalReferenceForbidden(DefusedXmlException): + """Resolving an external reference is forbidden""" + + def __init__(self, context, base, sysid, pubid): + super(ExternalReferenceForbidden, self).__init__() + self.context = context + self.base = base + self.sysid = sysid + self.pubid = pubid + + def __str__(self): + tpl = "ExternalReferenceForbidden(system_id='{}', public_id={})" + return tpl.format(self.sysid, self.pubid) + + +class NotSupportedError(DefusedXmlException): + """The operation is not supported""" + + +def _apply_defusing(defused_mod): + assert defused_mod is sys.modules[defused_mod.__name__] + stdlib_name = defused_mod.__origin__ + __import__(stdlib_name, {}, {}, ["*"]) + stdlib_mod = sys.modules[stdlib_name] + stdlib_names = set(dir(stdlib_mod)) + for name, obj in vars(defused_mod).items(): + if name.startswith("_") or name not in stdlib_names: + continue + setattr(stdlib_mod, name, obj) + return stdlib_mod + + +def _generate_etree_functions(DefusedXMLParser, _TreeBuilder, _parse, _iterparse): + """Factory for functions needed by etree, dependent on whether + cElementTree or ElementTree is used.""" + + def parse(source, parser=None, forbid_dtd=False, forbid_entities=True, forbid_external=True): + if parser is None: + parser = DefusedXMLParser( + target=_TreeBuilder(), + forbid_dtd=forbid_dtd, + forbid_entities=forbid_entities, + forbid_external=forbid_external, + ) + return _parse(source, parser) + + def iterparse( + source, + events=None, + parser=None, + forbid_dtd=False, + forbid_entities=True, + forbid_external=True, + ): + if parser is None: + parser = DefusedXMLParser( + target=_TreeBuilder(), + forbid_dtd=forbid_dtd, + forbid_entities=forbid_entities, + forbid_external=forbid_external, + ) + return _iterparse(source, events, parser) + + def fromstring(text, forbid_dtd=False, forbid_entities=True, forbid_external=True): + parser = DefusedXMLParser( + target=_TreeBuilder(), + forbid_dtd=forbid_dtd, + forbid_entities=forbid_entities, + forbid_external=forbid_external, + ) + parser.feed(text) + return parser.close() + + return parse, iterparse, fromstring diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatbuilder.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatbuilder.py new file mode 100644 index 0000000..7bfc57e --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatbuilder.py @@ -0,0 +1,107 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Defused xml.dom.expatbuilder +""" +from __future__ import print_function, absolute_import + +from xml.dom.expatbuilder import ExpatBuilder as _ExpatBuilder +from xml.dom.expatbuilder import Namespaces as _Namespaces + +from .common import DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden + +__origin__ = "xml.dom.expatbuilder" + + +class DefusedExpatBuilder(_ExpatBuilder): + """Defused document builder""" + + def __init__( + self, options=None, forbid_dtd=False, forbid_entities=True, forbid_external=True + ): + _ExpatBuilder.__init__(self, options) + self.forbid_dtd = forbid_dtd + self.forbid_entities = forbid_entities + self.forbid_external = forbid_external + + def defused_start_doctype_decl(self, name, sysid, pubid, has_internal_subset): + raise DTDForbidden(name, sysid, pubid) + + def defused_entity_decl( + self, name, is_parameter_entity, value, base, sysid, pubid, notation_name + ): + raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name) + + def defused_unparsed_entity_decl(self, name, base, sysid, pubid, notation_name): + # expat 1.2 + raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) # pragma: no cover + + def defused_external_entity_ref_handler(self, context, base, sysid, pubid): + raise ExternalReferenceForbidden(context, base, sysid, pubid) + + def install(self, parser): + _ExpatBuilder.install(self, parser) + + if self.forbid_dtd: + parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl + if self.forbid_entities: + # if self._options.entities: + parser.EntityDeclHandler = self.defused_entity_decl + parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl + if self.forbid_external: + parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler + + +class DefusedExpatBuilderNS(_Namespaces, DefusedExpatBuilder): + """Defused document builder that supports namespaces.""" + + def install(self, parser): + DefusedExpatBuilder.install(self, parser) + if self._options.namespace_declarations: + parser.StartNamespaceDeclHandler = self.start_namespace_decl_handler + + def reset(self): + DefusedExpatBuilder.reset(self) + self._initNamespaces() + + +def parse(file, namespaces=True, forbid_dtd=False, forbid_entities=True, forbid_external=True): + """Parse a document, returning the resulting Document node. + + 'file' may be either a file name or an open file object. + """ + if namespaces: + build_builder = DefusedExpatBuilderNS + else: + build_builder = DefusedExpatBuilder + builder = build_builder( + forbid_dtd=forbid_dtd, forbid_entities=forbid_entities, forbid_external=forbid_external + ) + + if isinstance(file, str): + fp = open(file, "rb") + try: + result = builder.parseFile(fp) + finally: + fp.close() + else: + result = builder.parseFile(file) + return result + + +def parseString( + string, namespaces=True, forbid_dtd=False, forbid_entities=True, forbid_external=True +): + """Parse a document from a string, returning the resulting + Document node. + """ + if namespaces: + build_builder = DefusedExpatBuilderNS + else: + build_builder = DefusedExpatBuilder + builder = build_builder( + forbid_dtd=forbid_dtd, forbid_entities=forbid_entities, forbid_external=forbid_external + ) + return builder.parseString(string) diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatreader.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatreader.py new file mode 100644 index 0000000..890e1d1 --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatreader.py @@ -0,0 +1,61 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Defused xml.sax.expatreader +""" +from __future__ import print_function, absolute_import + +from xml.sax.expatreader import ExpatParser as _ExpatParser + +from .common import DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden + +__origin__ = "xml.sax.expatreader" + + +class DefusedExpatParser(_ExpatParser): + """Defused SAX driver for the pyexpat C module.""" + + def __init__( + self, + namespaceHandling=0, + bufsize=2 ** 16 - 20, + forbid_dtd=False, + forbid_entities=True, + forbid_external=True, + ): + _ExpatParser.__init__(self, namespaceHandling, bufsize) + self.forbid_dtd = forbid_dtd + self.forbid_entities = forbid_entities + self.forbid_external = forbid_external + + def defused_start_doctype_decl(self, name, sysid, pubid, has_internal_subset): + raise DTDForbidden(name, sysid, pubid) + + def defused_entity_decl( + self, name, is_parameter_entity, value, base, sysid, pubid, notation_name + ): + raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name) + + def defused_unparsed_entity_decl(self, name, base, sysid, pubid, notation_name): + # expat 1.2 + raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) # pragma: no cover + + def defused_external_entity_ref_handler(self, context, base, sysid, pubid): + raise ExternalReferenceForbidden(context, base, sysid, pubid) + + def reset(self): + _ExpatParser.reset(self) + parser = self._parser + if self.forbid_dtd: + parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl + if self.forbid_entities: + parser.EntityDeclHandler = self.defused_entity_decl + parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl + if self.forbid_external: + parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler + + +def create_parser(*args, **kwargs): + return DefusedExpatParser(*args, **kwargs) diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/lxml.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/lxml.py new file mode 100644 index 0000000..99d5be9 --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/lxml.py @@ -0,0 +1,153 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""DEPRECATED Example code for lxml.etree protection + +The code has NO protection against decompression bombs. +""" +from __future__ import print_function, absolute_import + +import threading +import warnings + +from lxml import etree as _etree + +from .common import DTDForbidden, EntitiesForbidden, NotSupportedError + +LXML3 = _etree.LXML_VERSION[0] >= 3 + +__origin__ = "lxml.etree" + +tostring = _etree.tostring + + +warnings.warn( + "defusedxml.lxml is no longer supported and will be removed in a future release.", + category=DeprecationWarning, + stacklevel=2, +) + + +class RestrictedElement(_etree.ElementBase): + """A restricted Element class that filters out instances of some classes""" + + __slots__ = () + # blacklist = (etree._Entity, etree._ProcessingInstruction, etree._Comment) + blacklist = _etree._Entity + + def _filter(self, iterator): + blacklist = self.blacklist + for child in iterator: + if isinstance(child, blacklist): + continue + yield child + + def __iter__(self): + iterator = super(RestrictedElement, self).__iter__() + return self._filter(iterator) + + def iterchildren(self, tag=None, reversed=False): + iterator = super(RestrictedElement, self).iterchildren(tag=tag, reversed=reversed) + return self._filter(iterator) + + def iter(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iter(tag=tag, *tags) + return self._filter(iterator) + + def iterdescendants(self, tag=None, *tags): + iterator = super(RestrictedElement, self).iterdescendants(tag=tag, *tags) + return self._filter(iterator) + + def itersiblings(self, tag=None, preceding=False): + iterator = super(RestrictedElement, self).itersiblings(tag=tag, preceding=preceding) + return self._filter(iterator) + + def getchildren(self): + iterator = super(RestrictedElement, self).__iter__() + return list(self._filter(iterator)) + + def getiterator(self, tag=None): + iterator = super(RestrictedElement, self).getiterator(tag) + return self._filter(iterator) + + +class GlobalParserTLS(threading.local): + """Thread local context for custom parser instances""" + + parser_config = { + "resolve_entities": False, + # 'remove_comments': True, + # 'remove_pis': True, + } + + element_class = RestrictedElement + + def createDefaultParser(self): + parser = _etree.XMLParser(**self.parser_config) + element_class = self.element_class + if self.element_class is not None: + lookup = _etree.ElementDefaultClassLookup(element=element_class) + parser.set_element_class_lookup(lookup) + return parser + + def setDefaultParser(self, parser): + self._default_parser = parser + + def getDefaultParser(self): + parser = getattr(self, "_default_parser", None) + if parser is None: + parser = self.createDefaultParser() + self.setDefaultParser(parser) + return parser + + +_parser_tls = GlobalParserTLS() +getDefaultParser = _parser_tls.getDefaultParser + + +def check_docinfo(elementtree, forbid_dtd=False, forbid_entities=True): + """Check docinfo of an element tree for DTD and entity declarations + + The check for entity declarations needs lxml 3 or newer. lxml 2.x does + not support dtd.iterentities(). + """ + docinfo = elementtree.docinfo + if docinfo.doctype: + if forbid_dtd: + raise DTDForbidden(docinfo.doctype, docinfo.system_url, docinfo.public_id) + if forbid_entities and not LXML3: + # lxml < 3 has no iterentities() + raise NotSupportedError("Unable to check for entity declarations " "in lxml 2.x") + + if forbid_entities: + for dtd in docinfo.internalDTD, docinfo.externalDTD: + if dtd is None: + continue + for entity in dtd.iterentities(): + raise EntitiesForbidden(entity.name, entity.content, None, None, None, None) + + +def parse(source, parser=None, base_url=None, forbid_dtd=False, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + elementtree = _etree.parse(source, parser, base_url=base_url) + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return elementtree + + +def fromstring(text, parser=None, base_url=None, forbid_dtd=False, forbid_entities=True): + if parser is None: + parser = getDefaultParser() + rootelement = _etree.fromstring(text, parser, base_url=base_url) + elementtree = rootelement.getroottree() + check_docinfo(elementtree, forbid_dtd, forbid_entities) + return rootelement + + +XML = fromstring + + +def iterparse(*args, **kwargs): + raise NotSupportedError("defused lxml.etree.iterparse not available") diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/minidom.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/minidom.py new file mode 100644 index 0000000..78033b6 --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/minidom.py @@ -0,0 +1,63 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Defused xml.dom.minidom +""" +from __future__ import print_function, absolute_import + +from xml.dom.minidom import _do_pulldom_parse +from . import expatbuilder as _expatbuilder +from . import pulldom as _pulldom + +__origin__ = "xml.dom.minidom" + + +def parse( + file, parser=None, bufsize=None, forbid_dtd=False, forbid_entities=True, forbid_external=True +): + """Parse a file into a DOM by filename or file object.""" + if parser is None and not bufsize: + return _expatbuilder.parse( + file, + forbid_dtd=forbid_dtd, + forbid_entities=forbid_entities, + forbid_external=forbid_external, + ) + else: + return _do_pulldom_parse( + _pulldom.parse, + (file,), + { + "parser": parser, + "bufsize": bufsize, + "forbid_dtd": forbid_dtd, + "forbid_entities": forbid_entities, + "forbid_external": forbid_external, + }, + ) + + +def parseString( + string, parser=None, forbid_dtd=False, forbid_entities=True, forbid_external=True +): + """Parse a file into a DOM from a string.""" + if parser is None: + return _expatbuilder.parseString( + string, + forbid_dtd=forbid_dtd, + forbid_entities=forbid_entities, + forbid_external=forbid_external, + ) + else: + return _do_pulldom_parse( + _pulldom.parseString, + (string,), + { + "parser": parser, + "forbid_dtd": forbid_dtd, + "forbid_entities": forbid_entities, + "forbid_external": forbid_external, + }, + ) diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/pulldom.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/pulldom.py new file mode 100644 index 0000000..e3b10a4 --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/pulldom.py @@ -0,0 +1,41 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Defused xml.dom.pulldom +""" +from __future__ import print_function, absolute_import + +from xml.dom.pulldom import parse as _parse +from xml.dom.pulldom import parseString as _parseString +from .sax import make_parser + +__origin__ = "xml.dom.pulldom" + + +def parse( + stream_or_string, + parser=None, + bufsize=None, + forbid_dtd=False, + forbid_entities=True, + forbid_external=True, +): + if parser is None: + parser = make_parser() + parser.forbid_dtd = forbid_dtd + parser.forbid_entities = forbid_entities + parser.forbid_external = forbid_external + return _parse(stream_or_string, parser, bufsize) + + +def parseString( + string, parser=None, forbid_dtd=False, forbid_entities=True, forbid_external=True +): + if parser is None: + parser = make_parser() + parser.forbid_dtd = forbid_dtd + parser.forbid_entities = forbid_entities + parser.forbid_external = forbid_external + return _parseString(string, parser) diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/sax.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/sax.py new file mode 100644 index 0000000..b2786f7 --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/sax.py @@ -0,0 +1,60 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Defused xml.sax +""" +from __future__ import print_function, absolute_import + +from xml.sax import InputSource as _InputSource +from xml.sax import ErrorHandler as _ErrorHandler + +from . import expatreader + +__origin__ = "xml.sax" + + +def parse( + source, + handler, + errorHandler=_ErrorHandler(), + forbid_dtd=False, + forbid_entities=True, + forbid_external=True, +): + parser = make_parser() + parser.setContentHandler(handler) + parser.setErrorHandler(errorHandler) + parser.forbid_dtd = forbid_dtd + parser.forbid_entities = forbid_entities + parser.forbid_external = forbid_external + parser.parse(source) + + +def parseString( + string, + handler, + errorHandler=_ErrorHandler(), + forbid_dtd=False, + forbid_entities=True, + forbid_external=True, +): + from io import BytesIO + + if errorHandler is None: + errorHandler = _ErrorHandler() + parser = make_parser() + parser.setContentHandler(handler) + parser.setErrorHandler(errorHandler) + parser.forbid_dtd = forbid_dtd + parser.forbid_entities = forbid_entities + parser.forbid_external = forbid_external + + inpsrc = _InputSource() + inpsrc.setByteStream(BytesIO(string)) + parser.parse(inpsrc) + + +def make_parser(parser_list=[]): + return expatreader.create_parser() diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/xmlrpc.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/xmlrpc.py new file mode 100644 index 0000000..fbc674d --- /dev/null +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/xmlrpc.py @@ -0,0 +1,153 @@ +# defusedxml +# +# Copyright (c) 2013 by Christian Heimes +# Licensed to PSF under a Contributor Agreement. +# See https://www.python.org/psf/license for licensing details. +"""Defused xmlrpclib + +Also defuses gzip bomb +""" +from __future__ import print_function, absolute_import + +import io + +from .common import DTDForbidden, EntitiesForbidden, ExternalReferenceForbidden, PY3 + +if PY3: + __origin__ = "xmlrpc.client" + from xmlrpc.client import ExpatParser + from xmlrpc import client as xmlrpc_client + from xmlrpc import server as xmlrpc_server + from xmlrpc.client import gzip_decode as _orig_gzip_decode + from xmlrpc.client import GzipDecodedResponse as _OrigGzipDecodedResponse +else: + __origin__ = "xmlrpclib" + from xmlrpclib import ExpatParser + import xmlrpclib as xmlrpc_client + + xmlrpc_server = None + from xmlrpclib import gzip_decode as _orig_gzip_decode + from xmlrpclib import GzipDecodedResponse as _OrigGzipDecodedResponse + +try: + import gzip +except ImportError: # pragma: no cover + gzip = None + + +# Limit maximum request size to prevent resource exhaustion DoS +# Also used to limit maximum amount of gzip decoded data in order to prevent +# decompression bombs +# A value of -1 or smaller disables the limit +MAX_DATA = 30 * 1024 * 1024 # 30 MB + + +def defused_gzip_decode(data, limit=None): + """gzip encoded data -> unencoded data + + Decode data using the gzip content encoding as described in RFC 1952 + """ + if not gzip: # pragma: no cover + raise NotImplementedError + if limit is None: + limit = MAX_DATA + f = io.BytesIO(data) + gzf = gzip.GzipFile(mode="rb", fileobj=f) + try: + if limit < 0: # no limit + decoded = gzf.read() + else: + decoded = gzf.read(limit + 1) + except IOError: # pragma: no cover + raise ValueError("invalid data") + f.close() + gzf.close() + if limit >= 0 and len(decoded) > limit: + raise ValueError("max gzipped payload length exceeded") + return decoded + + +class DefusedGzipDecodedResponse(gzip.GzipFile if gzip else object): + """a file-like object to decode a response encoded with the gzip + method, as described in RFC 1952. + """ + + def __init__(self, response, limit=None): + # response doesn't support tell() and read(), required by + # GzipFile + if not gzip: # pragma: no cover + raise NotImplementedError + self.limit = limit = limit if limit is not None else MAX_DATA + if limit < 0: # no limit + data = response.read() + self.readlength = None + else: + data = response.read(limit + 1) + self.readlength = 0 + if limit >= 0 and len(data) > limit: + raise ValueError("max payload length exceeded") + self.stringio = io.BytesIO(data) + gzip.GzipFile.__init__(self, mode="rb", fileobj=self.stringio) + + def read(self, n): + if self.limit >= 0: + left = self.limit - self.readlength + n = min(n, left + 1) + data = gzip.GzipFile.read(self, n) + self.readlength += len(data) + if self.readlength > self.limit: + raise ValueError("max payload length exceeded") + return data + else: + return gzip.GzipFile.read(self, n) + + def close(self): + gzip.GzipFile.close(self) + self.stringio.close() + + +class DefusedExpatParser(ExpatParser): + def __init__(self, target, forbid_dtd=False, forbid_entities=True, forbid_external=True): + ExpatParser.__init__(self, target) + self.forbid_dtd = forbid_dtd + self.forbid_entities = forbid_entities + self.forbid_external = forbid_external + parser = self._parser + if self.forbid_dtd: + parser.StartDoctypeDeclHandler = self.defused_start_doctype_decl + if self.forbid_entities: + parser.EntityDeclHandler = self.defused_entity_decl + parser.UnparsedEntityDeclHandler = self.defused_unparsed_entity_decl + if self.forbid_external: + parser.ExternalEntityRefHandler = self.defused_external_entity_ref_handler + + def defused_start_doctype_decl(self, name, sysid, pubid, has_internal_subset): + raise DTDForbidden(name, sysid, pubid) + + def defused_entity_decl( + self, name, is_parameter_entity, value, base, sysid, pubid, notation_name + ): + raise EntitiesForbidden(name, value, base, sysid, pubid, notation_name) + + def defused_unparsed_entity_decl(self, name, base, sysid, pubid, notation_name): + # expat 1.2 + raise EntitiesForbidden(name, None, base, sysid, pubid, notation_name) # pragma: no cover + + def defused_external_entity_ref_handler(self, context, base, sysid, pubid): + raise ExternalReferenceForbidden(context, base, sysid, pubid) + + +def monkey_patch(): + xmlrpc_client.FastParser = DefusedExpatParser + xmlrpc_client.GzipDecodedResponse = DefusedGzipDecodedResponse + xmlrpc_client.gzip_decode = defused_gzip_decode + if xmlrpc_server: + xmlrpc_server.gzip_decode = defused_gzip_decode + + +def unmonkey_patch(): + xmlrpc_client.FastParser = None + xmlrpc_client.GzipDecodedResponse = _OrigGzipDecodedResponse + xmlrpc_client.gzip_decode = _orig_gzip_decode + if xmlrpc_server: + xmlrpc_server.gzip_decode = _orig_gzip_decode diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/exceptions.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/exceptions.py index b5719f1..1bb4992 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/exceptions.py +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/exceptions.py @@ -11,6 +11,7 @@ class ResponseException(RXVException): """Exception raised when yamaha receiver responded with an error code""" pass + ReponseException = ResponseException @@ -19,7 +20,25 @@ class MenuUnavailable(RXVException): pass +class MenuActionUnavailable(RXVException): + """Menu control action unavailable for current input""" + def __init__(self, input, action): + super().__init__(f'{input} does not support menu cursor {action}') + + class PlaybackUnavailable(RXVException): """Raised when playback function called on unsupported source.""" def __init__(self, source, action): - super(PlaybackUnavailable, self).__init__('{} does not support {}'.format(source, action)) + super().__init__('{} does not support {}'.format(source, action)) + + +class CommandUnavailable(RXVException): + """Raised when command is called on unsupported device.""" + def __init__(self, zone, command): + super().__init__('{} does not support {}'.format(zone, command)) + + +class UnknownPort(RXVException): + """Raised when an unknown port is found.""" + def __init__(self, port): + super().__init__('port {} is not supported'.format(port)) diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py index cc0c077..cc95100 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py @@ -7,13 +7,16 @@ import re import time import warnings -import xml.etree.ElementTree as ET +import xml from collections import namedtuple from math import floor import requests +from defusedxml import cElementTree -from .exceptions import MenuUnavailable, PlaybackUnavailable, ResponseException +from .exceptions import (CommandUnavailable, MenuUnavailable, + MenuActionUnavailable, PlaybackUnavailable, + ResponseException, UnknownPort) try: from urllib.parse import urlparse @@ -48,6 +51,7 @@ def __init__(self, play=False, stop=False, pause=False, YamahaCommand = '{payload}' Zone = '<{zone}>{request_text}' BasicStatusGet = 'GetParam' +PartyMode = '{state}' PowerControl = '{state}' PowerControlSleep = '{sleep_value}' Input = '{input_name}' @@ -60,18 +64,55 @@ def __init__(self, play=False, stop=False, pause=False, '' ListControlCursor = '<{src_name}>{action}'\ '' +CursorControlCursor = '<{src_name}>{action}'\ + '' VolumeLevel = '{value}' VolumeLevelValue = '{val}{exp}{unit}' VolumeMute = '{state}' +SoundVideo = '{value}' SelectNetRadioLine = 'Line_{lineno}'\ '' +SelectServerLine = 'Line_{lineno}'\ + '' + +HdmiOut = '{command}'\ + '' +AvailableScenes = 'GetParam' +Scene = '{parameter}' +SurroundProgram = '{parameter}' +DirectMode = '{parameter}' + +# String constants +STRAIGHT = "Straight" +DIRECT = "Direct" + +# PlayStatus options +ARTIST_OPTIONS = ["Artist", "Program_Type"] +ALBUM_OPTIONS = ["Album", "Radio_Text_A"] +SONG_OPTIONS = ["Song", "Track", "Radio_Text_B"] +STATION_OPTIONS = ["Station", "Program_Service"] + +# Cursor commands. +CURSOR_DISPLAY = "Display" +CURSOR_DOWN = "Down" +CURSOR_LEFT = "Left" +CURSOR_MENU = "Menu" +CURSOR_ON_SCREEN = "On Screen" +CURSOR_OPTION = "Option" +CURSOR_SEL = "Sel" +CURSOR_RETURN = "Return" +CURSOR_RETURN_TO_HOME = "Return to Home" +CURSOR_RIGHT = "Right" +CURSOR_TOP_MENU = "Top Menu" +CURSOR_UP = "Up" class RXV(object): def __init__(self, ctrl_url, model_name="Unknown", - zone="Main_Zone", friendly_name='Unknown', - unit_desc_url=None, timeout=5): + serial_number=None, zone="Main_Zone", + friendly_name='Unknown', unit_desc_url=None, + http_timeout=10.0): if re.match(r"\d{1,3}\.\d{1,3}\.\d{1,3}.\d{1,3}", ctrl_url): # backward compatibility: accept ip address as a contorl url warnings.warn("Using IP address as a Control URL is deprecated") @@ -79,20 +120,31 @@ def __init__(self, ctrl_url, model_name="Unknown", self.ctrl_url = ctrl_url self.unit_desc_url = unit_desc_url or re.sub('ctrl$', 'desc.xml', ctrl_url) self.model_name = model_name + self.serial_number = serial_number self.friendly_name = friendly_name + self.http_timeout = http_timeout self._inputs_cache = None self._zones_cache = None self._zone = zone - self.timeout = timeout + self._surround_programs_cache = None + self._scenes_cache = None self._session = requests.Session() self._discover_features() def _discover_features(self): """Pull and parse the desc.xml so we can query it later.""" try: - desc_xml = self._session.get(self.unit_desc_url, timeout=self.timeout).content - self._desc_xml = ET.fromstring(desc_xml) - except ET.ParseError: + desc_xml = self._session.get( + self.unit_desc_url, timeout=self.http_timeout + ).content + if not desc_xml: + logger.error( + "Unsupported Yamaha device? Failed to fetch {}".format( + self.unit_desc_url + )) + return + self._desc_xml = cElementTree.fromstring(desc_xml) + except xml.etree.ElementTree.ParseError: logger.exception("Invalid XML returned for request %s: %s", self.unit_desc_url, desc_xml) raise @@ -101,14 +153,12 @@ def _discover_features(self): raise def __unicode__(self): - return ('<{cls} model_name="{model}" zone="{zone}" ' - 'ctrl_url="{ctrl_url}" at {addr}>'.format( - cls=self.__class__.__name__, - zone=self._zone, - model=self.model_name, - ctrl_url=self.ctrl_url, - addr=hex(id(self)) - )) + return (f'<{self.__class__.__name__} ' + f'model_name="{self.model_name}" ' + f'serial_number="{self.serial_number}" ' + f'zone="{self._zone}" ' + f'ctrl_url="{self.ctrl_url}" ' + f'at {hex(id(self))}>') def __str__(self): return self.__unicode__() @@ -128,15 +178,16 @@ def _request(self, command, request_text, zone_cmd=True): self.ctrl_url, data=request_text, headers={"Content-Type": "text/xml"}, - timeout=self.timeout, + timeout=self.http_timeout ) - response = ET.XML(res.content) # releases connection to the pool + # releases connection to the pool + response = cElementTree.XML(res.content) if response.get("RC") != "0": logger.error("Request %s failed with %s", request_text, res.content) raise ResponseException(res.content) return response - except ET.ParseError: + except xml.etree.ElementTree.ParseError: logger.exception("Invalid XML returned for request %s: %s", request_text, res.content) raise @@ -249,6 +300,176 @@ def inputs(self): for elt in res.iter("Src_Name")))) return self._inputs_cache + @property + def outputs(self): + outputs = {} + + for cmd in self._find_commands('System,Sound_Video,HDMI,Output'): + # An output typically looks like this: + # System,Sound_Video,HDMI,Output,OUT_1 + # Extract the index number at the end as it is needed when + # requesting its current state. + m = re.match(r'.*_(\d+)$', cmd) + if m is None: + continue + + port_number = m.group(1) + request = HdmiOut.format(port=port_number, command='GetParam') + response = self._request('GET', request, zone_cmd=False) + port_state = response.find(cmd.replace(',', '/')).text.lower() + outputs['hdmi' + str(port_number)] = port_state + + return outputs + + def enable_output(self, port, enabled): + m = re.match(r'hdmi(\d+)', port.lower()) + if m is None: + raise UnknownPort(port) + + request = HdmiOut.format(port=m.group(1), + command='On' if enabled else 'Off') + self._request('PUT', request, zone_cmd=False) + + def _find_commands(self, cmd_name): + for cmd in self._desc_xml.findall('.//Cmd_List/Define'): + if cmd.text.startswith(cmd_name): + yield cmd.text + + @property + def direct_mode(self): + """ + Current state of direct mode. + """ + if DIRECT not in self.surround_programs(): + return False + + request_text = DirectMode.format(parameter="GetParam") + response = self._request('GET', request_text) + direct = response.find( + "%s/Sound_Video/Direct/Mode" % self.zone + ).text == "On" + + return direct + + @direct_mode.setter + def direct_mode(self, on): + """ + Enable/Disable direct mode. + + Precondition: DIRECT mode is supported, raises AssertionError otherwise. + """ + assert DIRECT in self.surround_programs() + if on: + request_text = DirectMode.format(parameter="On") + else: + request_text = DirectMode.format(parameter="Off") + self._request('PUT', request_text) + + @property + def surround_program(self): + """ + Get current selected surround program. + + If a STRAIGHT or DIRECT mode is supported and active, returns that mode. + Otherwise returns the currently active surround program. + """ + if self.direct_mode: + return DIRECT + + request_text = SurroundProgram.format(parameter=GetParam) + response = self._request('GET', request_text) + straight = response.find( + "%s/Surround/Program_Sel/Current/Straight" % self.zone + ).text == "On" + + if straight: + return STRAIGHT + + program = response.find( + "%s/Surround/Program_Sel/Current/Sound_Program" % self.zone + ).text + + return program + + @surround_program.setter + def surround_program(self, surround_name): + assert surround_name in self.surround_programs() + + # short circut on direct program + if surround_name == DIRECT: + self.direct_mode = True + return + + if self.direct_mode: + # Disable direct mode before changing any other settings, + # otherwise they don't have an effect + self.direct_mode = False + + if surround_name == STRAIGHT: + parameter = "On" + else: + parameter = "{parameter}".format( + parameter=surround_name + ) + request_text = SurroundProgram.format(parameter=parameter) + self._request('PUT', request_text) + + def surround_programs(self): + if not self._surround_programs_cache: + source_xml = self._desc_xml.find( + './/*[@YNC_Tag="%s"]' % self._zone + ) + if source_xml is None: + return False + + setup = source_xml.find('.//Menu[@Title_1="Setup"]') + if setup is None: + return False + + programs = setup.find('.//*[@Title_1="Program"]/Put_2/Param_1') + if programs is None: + return False + + supports = programs.findall('.//Direct') + self._surround_programs_cache = list() + for s in supports: + self._surround_programs_cache.append(s.text) + + straight = setup.find('.//*[@Title_1="Straight"]/Put_1') + if straight is not None: + self._surround_programs_cache.append(STRAIGHT) + + direct = setup.find('.//*[@Title_1="Direct"]/Put_1') + if direct is not None: + self._surround_programs_cache.append(DIRECT) + + return self._surround_programs_cache + + @property + def scene(self): + request_text = Scene.format(parameter=GetParam) + response = self._request('GET', request_text) + return response.find("%s/Scene/Scene_Sel" % self.zone).text + + @scene.setter + def scene(self, scene_name): + assert scene_name in self.scenes() + scene_number = self._scenes_cache.get(scene_name) + request_text = Scene.format(parameter=scene_number) + self._request('PUT', request_text) + + def scenes(self): + if not self._scenes_cache: + res = self._request('GET', AvailableScenes) + scenes = res.find('.//Scene') + if scenes is None: + return False + + self._scenes_cache = {} + for scene in scenes: + self._scenes_cache[scene.text] = scene.tag.replace("_", " ") + return self._scenes_cache + @property def zone(self): return self._zone @@ -311,6 +532,12 @@ def supports_play_method(self, source, method): def _src_name(self, cur_input): if cur_input not in self.inputs(): return None + if cur_input.upper().startswith('HDMI'): + # CEC commands can be sent over the HDMI inputs to control devices + # connected to the receiver. These can support play methods as well + # as menu cursor commands. Return the zone so these features + # will be enabled. + return self.zone return self.inputs()[cur_input] def is_ready(self): @@ -324,8 +551,28 @@ def is_ready(self): avail = next(config.iter('Feature_Availability')) return avail.text == 'Ready' + @staticmethod + def safe_get(doc, names): + try: + # python 3.x + import html + except ImportError: + # python 2.7 + import HTMLParser + html = HTMLParser.HTMLParser() + + for name in names: + tag = doc.find(".//%s" % name) + if tag is not None and tag.text is not None: + # Tuner and Net Radio sometimes respond + # with escaped entities + return html.unescape(tag.text).strip() + return "" + def play_status(self): + src_name = self._src_name(self.input) + if not src_name: return None @@ -335,21 +582,16 @@ def play_status(self): request_text = PlayGet.format(src_name=src_name) res = self._request('GET', request_text, zone_cmd=False) - playing = (res.find(".//Playback_Info").text == "Play") + playing = RXV.safe_get(res, ["Playback_Info"]) == "Play" \ + or src_name == "Tuner" - def safe_get(doc, name): - tag = doc.find(".//%s" % name) - if tag is not None: - return tag.text or "" - else: - return "" - - artist = safe_get(res, "Artist") - album = safe_get(res, "Album") - song = safe_get(res, "Song") - station = safe_get(res, "Station") - - status = PlayStatus(playing, artist, album, song, station) + status = PlayStatus( + playing, + artist=RXV.safe_get(res, ARTIST_OPTIONS), + album=RXV.safe_get(res, ALBUM_OPTIONS), + song=RXV.safe_get(res, SONG_OPTIONS), + station=RXV.safe_get(res, STATION_OPTIONS) + ) return status def menu_status(self): @@ -370,7 +612,7 @@ def menu_status(self): cl = { elt.tag: elt.find('Txt').text - for elt in current_list.getchildren() + for elt in list(current_list) if elt.find('Attribute').text != 'Unselectable' } @@ -383,35 +625,86 @@ def menu_jump_line(self, lineno): if not src_name: raise MenuUnavailable(cur_input) - request_text = ListControlJumpLine.format(src_name=src_name, lineno=lineno) + request_text = ListControlJumpLine.format( + src_name=src_name, + lineno=lineno + ) return self._request('PUT', request_text, zone_cmd=False) + def supported_cursor_actions(self, cur_input=None): + if cur_input is None: + cur_input = self.input + src_name = self._src_name(cur_input) + if not src_name: + return frozenset() + cursor_actions = self._desc_xml.findall( + f'.//*[@YNC_Tag="{src_name}"]//Menu[@Func="Cursor"]/Put_1') + if cursor_actions is None: + return frozenset() + return frozenset(action.text for action in cursor_actions) + def _menu_cursor(self, action): cur_input = self.input src_name = self._src_name(cur_input) if not src_name: raise MenuUnavailable(cur_input) - request_text = ListControlCursor.format(src_name=src_name, action=action) + if self.supports_method(src_name, 'List_Control', 'Cursor'): + template = ListControlCursor + elif self.supports_method(src_name, 'Cursor_Control', 'Cursor'): + template = CursorControlCursor + else: + raise MenuUnavailable(cur_input) + + # Check that the specific action is available for the input. + if action not in self.supported_cursor_actions(): + raise MenuActionUnavailable(cur_input, action) + + request_text = template.format( + src_name=src_name, + action=action + ) return self._request('PUT', request_text, zone_cmd=False) def menu_up(self): - return self._menu_cursor("Up") + return self._menu_cursor(CURSOR_UP) def menu_down(self): - return self._menu_cursor("Down") + return self._menu_cursor(CURSOR_DOWN) def menu_left(self): - return self._menu_cursor("Left") + return self._menu_cursor(CURSOR_LEFT) def menu_right(self): - return self._menu_cursor("Right") + return self._menu_cursor(CURSOR_RIGHT) def menu_sel(self): - return self._menu_cursor("Sel") + return self._menu_cursor(CURSOR_SEL) def menu_return(self): - return self._menu_cursor("Return") + return self._menu_cursor(CURSOR_RETURN) + + def menu_return_to_home(self): + return self._menu_cursor(CURSOR_RETURN_TO_HOME) + + def menu_on_screen(self): + return self._menu_cursor(CURSOR_ON_SCREEN) + + def menu_top_menu(self): + return self._menu_cursor(CURSOR_TOP_MENU) + + def menu_menu(self): + return self._menu_cursor(CURSOR_MENU) + + def menu_option(self): + return self._menu_cursor(CURSOR_OPTION) + + def menu_display(self): + return self._menu_cursor(CURSOR_DISPLAY) + + def menu_reset(self): + while self.menu_status().layer > 1: + self.menu_return() @property def volume(self): @@ -422,7 +715,17 @@ def volume(self): @volume.setter def volume(self, value): - value = str(int(value * 10)) + """Convert volume for setting. + + We're passing around volume in standard db units, like -52.0 + db. The API takes int values. However, the API also only takes + int values that corespond to half db steps (so -52.0 and -51.5 + are valid, -51.8 is not). + + Through the power of math doing the int of * 2, then * 5 will + ensure we only get half steps. + """ + value = str(int(value * 2) * 5) exp = 1 unit = 'dB' @@ -439,6 +742,22 @@ def volume_fade(self, final_vol, sleep=0.5): self.volume = val time.sleep(sleep) + @property + def partymode(self): + request_text = PartyMode.format(state=GetParam) + response = self._request('GET', request_text, False) + pmode = response.find('System/Party_Mode/Mode').text + assert pmode in ["On", "Off"] + return pmode == "On" + + @partymode.setter + def partymode(self, state): + assert state in [True, False] + new_state = "On" if state else "Off" + request_text = PartyMode.format(state=new_state) + response = self._request('PUT', request_text, False) + return response + @property def mute(self): request_text = VolumeMute.format(state=GetParam) @@ -455,6 +774,70 @@ def mute(self, state): response = self._request('PUT', request_text) return response + @property + def adaptive_drc(self): + """ + View the current Adaptive Dynamic Range Compression setting, a means + of equalizing various input levels at low volume. This feature is ideal + for watching late at night (to avoid extremes of volume between + dialogue scenes and explosions etc.) or in noisy environments. It is + best disabled for the full dynamic range audio experience. + + :return: True if Dynamic Range Compression is enabled. + """ + get_tag = 'GetParam' + request_text = SoundVideo.format(value=get_tag) + response = self._request('GET', request_text) + drc = response.find('%s/Sound_Video/Adaptive_DRC' % self.zone).text + return False if drc == 'Off' else True + + @adaptive_drc.setter + def adaptive_drc(self, value=False): + """ + :param value: True to enable dynamic range compression. Default False. + """ + set_value = 'Auto' if value else 'Off' + set_tag = '{}'.format(set_value) + request_text = SoundVideo.format(value=set_tag) + self._request('PUT', request_text) + + @property + def dialogue_level(self): + """ + An adjustment to elevate the volume of dialogue sounds; useful if the + volume of dialogue is difficult to make out against background sounds + or music. + + :return: An integer between 0 (no adjustment) to 3 (most increased). + """ + if self.supports_method(self.zone, "Sound_Video", "Dialogue_Adjust", "Dialogue_Lvl"): + raise CommandUnavailable(self.zone, "Dialogue_Lvl") + + get_tag = 'GetParam' \ + '' + request_text = SoundVideo.format(value=get_tag) + response = self._request('GET', request_text) + level = response.find('%s/Sound_Video/Dialogue_Adjust/Dialogue_Lvl' + % self.zone).text + return int(level) + + @dialogue_level.setter + def dialogue_level(self, value=0): + """ + :param value: An integer between 0 and 3 to determine how much to + increase dialogue sounds over other sounds. A value of zero + disables this feature. + """ + if self.supports_method(self.zone, "Sound_Video", "Dialogue_Adjust", "Dialogue_Lvl"): + raise CommandUnavailable(self.zone, "Dialogue_Lvl") + + if int(value) not in [0, 1, 2, 3]: + raise ValueError("Value must be 0, 1, 2, or 3") + set_tag = '{}' \ + ''.format(int(value)) + request_text = SoundVideo.format(value=set_tag) + self._request('PUT', request_text) + def _direct_sel(self, lineno): request_text = SelectNetRadioLine.format(lineno=lineno) return self._request('PUT', request_text, zone_cmd=False) @@ -473,9 +856,11 @@ def net_radio(self, path): ensure we give it time to get there. TODO: better error handling if we some how time out + TODO: multi page menus (scrolling down) """ layers = path.split(">") self.input = "NET RADIO" + self.menu_reset() for attempt in range(20): menu = self.menu_status() @@ -491,6 +876,39 @@ def net_radio(self, path): # print("Sleeping because we are not ready yet") time.sleep(1) + def _direct_sel_server(self, lineno): + request_text = SelectServerLine.format(lineno=lineno) + return self._request('PUT', request_text, zone_cmd=False) + + def server(self, path): + """Play from specified server + + This lets you play a SERVER address in a single command + with by encoding it with > as separators. For instance: + + Server>Playlists>GoodVibes + + This code is copied from the net_radio function. + + TODO: better error handling if we some how time out + """ + layers = path.split(">") + self.input = "SERVER" + + for attempt in range(20): + menu = self.menu_status() + if menu.ready: + for line, value in menu.current_list.items(): + if value == layers[menu.layer - 1]: + lineno = line[5:] + self._direct_sel_server(lineno) + if menu.layer == len(layers): + return + break + else: + # print("Sleeping because we are not ready yet") + time.sleep(1) + @property def sleep(self): request_text = PowerControlSleep.format(sleep_value=GetParam) diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py index 7be95a3..180c0af 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py @@ -4,10 +4,11 @@ import re import socket -import xml.etree.ElementTree as ET +import xml from collections import namedtuple import requests +from defusedxml import cElementTree try: from urllib.parse import urljoin @@ -36,8 +37,15 @@ "{urn:schemas-upnp-org:device-1-0}device" "/{urn:schemas-upnp-org:device-1-0}friendlyName" ) +SERIAL_NUMBER_QUERY = ( + "{urn:schemas-upnp-org:device-1-0}device" + "/{urn:schemas-upnp-org:device-1-0}serialNumber" +) -RxvDetails = namedtuple("RxvDetails", "ctrl_url unit_desc_url, model_name friendly_name") +RxvDetails = namedtuple( + "RxvDetails", + "ctrl_url unit_desc_url, model_name friendly_name serial_number" +) def discover(timeout=1.5): @@ -72,20 +80,21 @@ def rxv_details(location): """Looks under given UPNP url, and checks if Yamaha amplituner lives there returns RxvDetails if yes, None otherwise""" try: - xml = ET.XML(requests.get(location).content) - except: + res = cElementTree.XML(requests.get(location).content) + except xml.etree.ElementTree.ParseError: return None - url_base_el = xml.find(URL_BASE_QUERY) + url_base_el = res.find(URL_BASE_QUERY) if url_base_el is None: return None - ctrl_url_local = xml.find(CONTROL_URL_QUERY).text + ctrl_url_local = res.find(CONTROL_URL_QUERY).text ctrl_url = urljoin(url_base_el.text, ctrl_url_local) - unit_desc_url_local = xml.find(UNITDESC_URL_QUERY).text + unit_desc_url_local = res.find(UNITDESC_URL_QUERY).text unit_desc_url = urljoin(url_base_el.text, unit_desc_url_local) - model_name = xml.find(MODEL_NAME_QUERY).text - friendly_name = xml.find(FRIENDLY_NAME_QUERY).text + model_name = res.find(MODEL_NAME_QUERY).text + friendly_name = res.find(FRIENDLY_NAME_QUERY).text + serial_number = res.find(SERIAL_NUMBER_QUERY).text - return RxvDetails(ctrl_url, unit_desc_url, model_name, friendly_name) + return RxvDetails(ctrl_url, unit_desc_url, model_name, friendly_name, serial_number) if __name__ == '__main__': diff --git a/Yahama RX.indigoPlugin/Contents/__init__.py b/Yahama RX.indigoPlugin/Contents/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Yahama RX.indigoPlugin/__init__.py b/Yahama RX.indigoPlugin/__init__.py new file mode 100644 index 0000000..e69de29 From 6afc19e6db2963da1194a5a289da379003580ece Mon Sep 17 00:00:00 2001 From: Lucas Nealan Date: Fri, 14 Jul 2023 00:08:24 -0700 Subject: [PATCH 2/3] Fix issues with defused xml and startup error --- Yahama RX.indigoPlugin/Contents/Info.plist | 2 +- Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py | 2 +- .../Contents/Server Plugin/rxv/defusedxml/common.py | 1 + Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py | 6 +++--- Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py | 4 ++-- 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Yahama RX.indigoPlugin/Contents/Info.plist b/Yahama RX.indigoPlugin/Contents/Info.plist index b989e6e..259738b 100644 --- a/Yahama RX.indigoPlugin/Contents/Info.plist +++ b/Yahama RX.indigoPlugin/Contents/Info.plist @@ -3,7 +3,7 @@ PluginVersion - 2022.0.0 + 2022.0.2 ServerApiVersion 3.0 IwsApiVersion diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py index 5dd759f..ba9bd27 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py @@ -259,7 +259,7 @@ def refresh_receiver_list(self, filter="", valuesDict=None, typeId="", targetId= # # Updates the status for the specified device. ############### - def update_status(self, dev_id): + def updateStatus(self, dev_id): devTup = self.devices.get(dev_id, None) if devTup: dev = devTup[0] diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py index 5ceda1f..e82d57a 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py @@ -118,6 +118,7 @@ def iterparse( def fromstring(text, forbid_dtd=False, forbid_entities=True, forbid_external=True): parser = DefusedXMLParser( + encoding="UTF-8", target=_TreeBuilder(), forbid_dtd=forbid_dtd, forbid_entities=forbid_entities, diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py index cc95100..06c95c6 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py @@ -12,7 +12,7 @@ from math import floor import requests -from defusedxml import cElementTree +from .defusedxml import ElementTree from .exceptions import (CommandUnavailable, MenuUnavailable, MenuActionUnavailable, PlaybackUnavailable, @@ -143,7 +143,7 @@ def _discover_features(self): self.unit_desc_url )) return - self._desc_xml = cElementTree.fromstring(desc_xml) + self._desc_xml = ElementTree.fromstring(desc_xml) except xml.etree.ElementTree.ParseError: logger.exception("Invalid XML returned for request %s: %s", self.unit_desc_url, desc_xml) @@ -181,7 +181,7 @@ def _request(self, command, request_text, zone_cmd=True): timeout=self.http_timeout ) # releases connection to the pool - response = cElementTree.XML(res.content) + response = ElementTree.XML(res.content) if response.get("RC") != "0": logger.error("Request %s failed with %s", request_text, res.content) diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py index 180c0af..041d28b 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py +++ b/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py @@ -8,7 +8,7 @@ from collections import namedtuple import requests -from defusedxml import cElementTree +from .defusedxml import ElementTree try: from urllib.parse import urljoin @@ -80,7 +80,7 @@ def rxv_details(location): """Looks under given UPNP url, and checks if Yamaha amplituner lives there returns RxvDetails if yes, None otherwise""" try: - res = cElementTree.XML(requests.get(location).content) + res = ElementTree.XML(requests.get(location).content) except xml.etree.ElementTree.ParseError: return None url_base_el = res.find(URL_BASE_QUERY) From 028284bfb60c0bba429f0301f02e44e680eabcfd Mon Sep 17 00:00:00 2001 From: Jon Date: Sat, 30 Sep 2023 10:40:08 +0100 Subject: [PATCH 3/3] Change plugin file name to YamahaRX Remove space from plugin file name. --- .gitignore | 1 + .../Contents/Info.plist | 2 +- .../Contents/Resources/icon.png | Bin .../Contents/Server Plugin/Actions.xml | 0 .../Contents/Server Plugin/Devices.xml | 0 .../Contents/Server Plugin/PluginConfig.xml | 0 .../Contents/Server Plugin/__init__.py | 0 .../Contents/Server Plugin/plugin.py | 18 +- .../Contents/Server Plugin/plugin.py.bak | 487 ++++++++++++++++++ .../Contents/Server Plugin/rxv/.gitignore | 0 .../Contents/Server Plugin/rxv/__init__.py | 0 .../rxv/defusedxml/ElementTree.py | 0 .../Server Plugin/rxv/defusedxml/__init__.py | 0 .../rxv/defusedxml/cElementTree.py | 0 .../Server Plugin/rxv/defusedxml/common.py | 0 .../rxv/defusedxml/expatbuilder.py | 0 .../rxv/defusedxml/expatreader.py | 0 .../Server Plugin/rxv/defusedxml/lxml.py | 0 .../Server Plugin/rxv/defusedxml/minidom.py | 0 .../Server Plugin/rxv/defusedxml/pulldom.py | 0 .../Server Plugin/rxv/defusedxml/sax.py | 0 .../Server Plugin/rxv/defusedxml/xmlrpc.py | 0 .../Contents/Server Plugin/rxv/exceptions.py | 0 .../Contents/Server Plugin/rxv/rxv.py | 0 .../Contents/Server Plugin/rxv/ssdp.py | 6 +- .../Contents/__init__.py | 0 .../__init__.py | 0 27 files changed, 500 insertions(+), 14 deletions(-) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Info.plist (96%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Resources/icon.png (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/Actions.xml (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/Devices.xml (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/PluginConfig.xml (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/__init__.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/plugin.py (98%) create mode 100644 YamahaRX.indigoPlugin/Contents/Server Plugin/plugin.py.bak rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/.gitignore (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/__init__.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/ElementTree.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/__init__.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/cElementTree.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/common.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/expatbuilder.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/expatreader.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/lxml.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/minidom.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/pulldom.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/sax.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/defusedxml/xmlrpc.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/exceptions.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/rxv.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/Server Plugin/rxv/ssdp.py (96%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/Contents/__init__.py (100%) rename {Yahama RX.indigoPlugin => YamahaRX.indigoPlugin}/__init__.py (100%) diff --git a/.gitignore b/.gitignore index 974e9b0..40e37d3 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ pip-log.txt #Mr Developer .mr.developer.cfg .idea/ +.DS_Store diff --git a/Yahama RX.indigoPlugin/Contents/Info.plist b/YamahaRX.indigoPlugin/Contents/Info.plist similarity index 96% rename from Yahama RX.indigoPlugin/Contents/Info.plist rename to YamahaRX.indigoPlugin/Contents/Info.plist index 259738b..5d0a7eb 100644 --- a/Yahama RX.indigoPlugin/Contents/Info.plist +++ b/YamahaRX.indigoPlugin/Contents/Info.plist @@ -3,7 +3,7 @@ PluginVersion - 2022.0.2 + 2022.0.3 ServerApiVersion 3.0 IwsApiVersion diff --git a/Yahama RX.indigoPlugin/Contents/Resources/icon.png b/YamahaRX.indigoPlugin/Contents/Resources/icon.png similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Resources/icon.png rename to YamahaRX.indigoPlugin/Contents/Resources/icon.png diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/Actions.xml b/YamahaRX.indigoPlugin/Contents/Server Plugin/Actions.xml similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/Actions.xml rename to YamahaRX.indigoPlugin/Contents/Server Plugin/Actions.xml diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/Devices.xml b/YamahaRX.indigoPlugin/Contents/Server Plugin/Devices.xml similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/Devices.xml rename to YamahaRX.indigoPlugin/Contents/Server Plugin/Devices.xml diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/PluginConfig.xml b/YamahaRX.indigoPlugin/Contents/Server Plugin/PluginConfig.xml similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/PluginConfig.xml rename to YamahaRX.indigoPlugin/Contents/Server Plugin/PluginConfig.xml diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/__init__.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/__init__.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/__init__.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/__init__.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/plugin.py similarity index 98% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/plugin.py index ba9bd27..905854c 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/plugin.py +++ b/YamahaRX.indigoPlugin/Contents/Server Plugin/plugin.py @@ -11,9 +11,9 @@ from rxv import exceptions as rxv_exceptions try: - import xml.etree.cElementTree as ET + import xml.etree.cElementTree as et except ImportError: - import xml.etree.ElementTree as ET + import xml.etree.ElementTree as et from xml.etree.ElementTree import ParseError @@ -64,7 +64,7 @@ def xmitToReceiver(dev, xml_string): headers={'Content-Type': 'application/xml'}) resp = urllib.request.urlopen(req) status_xml = resp.read() - root = ET.fromstring(status_xml) + root = et.fromstring(status_xml) return root @staticmethod @@ -139,7 +139,6 @@ def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): indigo.PluginBase.__init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs) self.debug = pluginPrefs.get("showDebugInfo", False) self.devices = {} - self.refresh_receiver_list() ################################## # Standard plugin operation methods @@ -147,6 +146,8 @@ def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): def startup(self): self.logger.debug("startup called") + self.refresh_receiver_list() + def shutdown(self): self.logger.debug("shutdown called") @@ -268,7 +269,8 @@ def updateStatus(self, dev_id): root = ClassicReceiver.xmitToReceiver(dev, xml_string) power = root.find("./Main_Zone/Basic_Status/Power_Control/Power").text sleep = root.find("./Main_Zone/Basic_Status/Power_Control/Sleep").text - if(sleep!='Off'): sleep = "n"+sleep + if sleep != 'Off': + sleep = "n"+sleep volume = root.find("./Main_Zone/Basic_Status/Vol/Lvl/Val").text mute = root.find("./Main_Zone/Basic_Status/Vol/Mute").text inputmode = root.find("./Main_Zone/Basic_Status/Input/Input_Sel").text @@ -346,7 +348,7 @@ def _set_rxv_property(self, dev, property, value): self.logger.debug(f"device '{dev.name}' had a connection error") self.logger.error(f"device '{dev.name}' is unavailable") except rxv_exceptions.ResponseException as e: - response = ET.XML(str(e)) + response = et.XML(str(e)) if response.get("RC") == "3": # RC 3 is what happens when you issue a command that's not valid - wrong menu navigation direction, etc. # We just skip it. @@ -354,10 +356,10 @@ def _set_rxv_property(self, dev, property, value): elif response.get("RC") != "4": self.logger.error(f"device '{dev.name}' can't have property '{property}' set to value '{str(value)}'") else: - # RC 4 is what happens when the amp is offline or in standby and you try to send it a command other than + # RC 4 is what happens when the amp is offline or in standby, and you try to send it a command other than # to turn on (if in standby) self.logger.error(f"device '{dev.name}' is unavailable") - except ET.ParseError: + except et.ParseError: # dev.setErrorStateOnServer('unavailable') # self.logger.debug(f"device '{dev.name}' failed to update status with an XML parse error") # This seems to happen relatively frequently - apparently sometimes the amp goes out to lunch for diff --git a/YamahaRX.indigoPlugin/Contents/Server Plugin/plugin.py.bak b/YamahaRX.indigoPlugin/Contents/Server Plugin/plugin.py.bak new file mode 100644 index 0000000..1726993 --- /dev/null +++ b/YamahaRX.indigoPlugin/Contents/Server Plugin/plugin.py.bak @@ -0,0 +1,487 @@ +#! /usr/bin/env python +# -*- coding: utf-8 -*- + +import urllib2 +import traceback + +import requests +from requests.exceptions import ConnectionError, ConnectTimeout, ReadTimeout + +import rxv +from rxv import exceptions as rxv_exceptions + +try: + import xml.etree.cElementTree as ET +except ImportError: + import xml.etree.ElementTree as ET + +from xml.etree.ElementTree import ParseError + +try: + import indigo +except: + pass + +kSleepBetweenUpdatePolls = 2 # number of seconds to sleep between polling each receiver for current status +kSleepValueMap = { + "Off": "Off", + "120 min": "n120", + "90 min": "n90", + "60 min": "n60", + "30 min": "n30", +} + +def str2bool(v): + return v.lower() in ("yes", "true", "t", "1", "on") + +class ClassicReceiver(object): + kInputList = ( + ('tuner', 'TUNER'), + ('multi_ch', 'MULTI CH'), + ('phono', 'PHONO'), + ('cd', 'CD'), + ('tv', 'TV'), + ('md.cd-r', 'MD/CD-R'), + ('bd.hd_dvd', 'BD/HD DVD'), + ('dvd', 'DVD'), + ('cbl.sat', 'CBL/SAT'), + ('dvr', 'DVR'), + ('vcr', 'VCR'), + ('v-aux', 'V-AUX'), + ('dock', 'DOCK'), + ('pc.mcx', 'PC/MCX'), + ('net_radio', 'NET RADIO'), + ('usb', 'USB'), + ) + @staticmethod + def xmitToReceiver(dev, xml_string): + url = 'http://' + dev.pluginProps['txtip'] + '/YamahaRemoteControl/ctrl' + + req = urllib2.Request( + url=url, + data=xml_string, + headers={'Content-Type': 'application/xml'}) + resp = urllib2.urlopen(req) + status_xml = resp.read() + root = ET.fromstring(status_xml) + return root + + @staticmethod + def putMute(logger, dev, val): + if dev is None: + logger.debug(u"no device defined") + return + + if val is None: + logger.debug(u"value not defined") + return + + xml_string = ''+val+'' + root = ClassicReceiver.xmitToReceiver( dev, xml_string) + + @staticmethod + def putVolume(logger, dev, val): + if dev is None: + logger.debug(u"no device defined") + return + + if val is None: + logger.debug(u"value not defined") + return + + xml_string = ''+val+'1dB' + root = ClassicReceiver.xmitToReceiver( dev, xml_string) + + @staticmethod + def putPower(logger, dev, val): + if dev is None: + logger.debug(u"no device defined") + return + + if val is None: + logger.debug(u"value not defined") + return + + xml_string = ''+val+'' + root = ClassicReceiver.xmitToReceiver( dev, xml_string) + + @staticmethod + def putSleep(logger, dev, val): + if dev is None: + logger.debug(u"no device defined") + return + + if val is None: + logger.debug(u"value not defined") + return + + xml_string = ''+val+'' + root = ClassicReceiver.xmitToReceiver( dev, xml_string) + + @staticmethod + def putInput(logger, dev, val): + if dev is None: + logger.debug(u"no device defined") + return + + if val is None: + logger.debug(u"value not defined") + return + + xml_string = ''+val+'' + root = ClassicReceiver.xmitToReceiver( dev, xml_string) + + + +class Plugin(indigo.PluginBase): + + def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs): + indigo.PluginBase.__init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs) + self.debug = pluginPrefs.get("showDebugInfo", False) + self.devices = {} + self.refresh_receiver_list() + + ################################## + # Standard plugin operation methods + ################################## + def startup(self): + self.logger.debug(u"startup called") + + def shutdown(self): + self.logger.debug(u"shutdown called") + + def deviceStartComm(self, dev): + if dev.deviceTypeId == "receiver": + devTup = (dev,) + elif dev.deviceTypeId == "rxvX73": + try: + rxv_obj = self.receivers.get(dev.pluginProps["control-url"], None) + if not rxv_obj and len(dev.pluginProps["manual-ip"]): + rxv_obj = rxv.RXV(ctrl_url=dev.pluginProps["manual-ip"]) + devTup = (dev, rxv_obj) + except (ConnectTimeout, ReadTimeout, ConnectionError) as e: + dev.setErrorStateOnServer('unavailable') + if isinstance(e, ConnectTimeout) or isinstance(e, ReadTimeout): + self.logger.debug("device '%s' connection timed out" % dev.name) + else: + self.logger.debug("device '%s' had a connection error" % dev.name) + return + except ParseError: + # This seems to happen relatively frequently - apparently sometimes the amp goes out to lunch for + # a bit (causing connection errors) and when it comes back it doesn't always return correct XML. + # I think the better idea here is to just pass on these errors as it always seems to resolve itself. + dev.setErrorStateOnServer('unavailable') + devTup = (dev, None) + except Exception as e: + self.logger.error("Couldn't start device %s - it may not be available on the network or the IP address may have changed." % dev.name) + self.logger.debug("exception starting device:\n%s" % traceback.format_exc(10)) + return + self.devices[dev.id] = devTup + self.updateStatus(dev.id) + + def deviceStopComm(self, dev): + try: + del self.devices[dev.id] + except: + pass + + def runConcurrentThread(self): + try: + while True: + for devId in self.devices.keys(): + self.updateStatus(devId) + self.sleep(2) + except self.StopThread: + return + except Exception as e: + self.logger.error("runConcurrentThread error: \n%s" % traceback.format_exc(10)) + + ################################## + # Config dialog methods + ################################## + ############### + # get_receiver_list + # + # Returns the list of receivers as a tuple for the device config dialog. + ############### + def get_receiver_list(self, filter="", valuesDict=None, typeId="", targetId=0): + return [(k, v.friendly_name) for k, v in self.receivers.iteritems()] + + def get_input_list(self, filter="", valuesDict=None, typeId="", targetId=0): + dev = indigo.devices.get(targetId, None) + if dev.deviceTypeId == 'receiver': + return ClassicReceiver.kInputList + elif dev.deviceTypeId == "rxvX73": + dev, rxv_obj = self.devices.get(dev.id, (None, None)) + if rxv_obj: + return [(k.replace("iPod (USB)", "iPod").replace(" ", "_"), k) for k in rxv_obj.inputs().keys()] + return [] + + def get_zone_list(self, filter="", valuesDict=None, typeId="", targetId=0): + dev = indigo.devices.get(targetId, None) + d, rxv_obj = self.devices.get(dev.id, (None, None)) + if rxv_obj: + return [(k, k) for k in rxv_obj.zones()] + return [] + + ######################################## + # Prefs dialog methods + ######################################## + def closedPrefsConfigUi(self, valuesDict, userCancelled): + # Since the dialog closed we want to set the debug flag - if you don't directly use + # a plugin's properties (and for debugLog we don't) you'll want to translate it to + # the appropriate stuff here. + if not userCancelled: + self.debug = valuesDict.get("showDebugInfo", False) + if self.debug: + indigo.server.log("Debug logging enabled") + else: + indigo.server.log("Debug logging disabled") + + ################################## + # Helper methods + ################################## + + ############### + # refresh_receiver_list + # + # Refreshes the self.receivers list from the SSDP query. It's automatically loaded at plugin launch, but we provide + # this method just in case we need to refresh the list. We also have a button in the config dialog that calls this + # so that we can force the list to refresh in the dialog itself. We don't refresh automatically because it takes + # a couple of seconds for the method to finish so the dialog looks like it's hung up. + ############### + def refresh_receiver_list(self, filter="", valuesDict=None, typeId="", targetId=0): + self.receivers = {r.ctrl_url: r for r in rxv.find()} + self.logger.debug("receivers list: %s" % unicode(self.receivers)) + + ############### + # updateStatus + # + # Updates the status for the specified device. + ############### + def updateStatus(self, dev_id): + devTup = self.devices.get(dev_id, None) + if devTup: + dev = devTup[0] + if dev.deviceTypeId == "receiver": + xml_string = 'GetParam' + root = ClassicReceiver.xmitToReceiver(dev, xml_string) + power = root.find("./Main_Zone/Basic_Status/Power_Control/Power").text + sleep = root.find("./Main_Zone/Basic_Status/Power_Control/Sleep").text + if(sleep!='Off'): sleep = "n"+sleep + volume = root.find("./Main_Zone/Basic_Status/Vol/Lvl/Val").text + mute = root.find("./Main_Zone/Basic_Status/Vol/Mute").text + inputmode = root.find("./Main_Zone/Basic_Status/Input/Input_Sel").text + state_list = [ + {"key": "power", "value": power}, + {"key": "sleep", "value": sleep}, + {"key": "volume", "value": volume}, + {"key": "mute", "value": mute}, + {"key": "input", "value": inputmode} + ] + dev.updateStatesOnServer(state_list) + elif dev.deviceTypeId == "rxvX73": + rxv_obj = devTup[1] + if not rxv_obj: + self.refresh_receiver_list() + rxv_obj = self.receivers.get(dev.pluginProps["control-url"], None) + if not rxv_obj: + dev.setErrorStateOnServer("unavailable") + return + else: + self.devices[dev.id] = (dev, rxv_obj) + try: + status = rxv_obj.basic_status + state_list = [ + {"key": "power", "value": status.on}, + {"key": "volume", "value": status.volume}, + {"key": "mute", "value": status.mute}, + {"key": "input", "value": status.input.replace(" ", "_")} + ] + dev.updateStatesOnServer(state_list) + dev.updateStateOnServer(key="sleep", value=kSleepValueMap[rxv_obj.sleep]) + if dev.errorState: + dev.setErrorStateOnServer(None) + except (ConnectTimeout, ReadTimeout, ConnectionError) as e: + dev.setErrorStateOnServer('unavailable') + if isinstance(e, ConnectTimeout) or isinstance(e, ReadTimeout): + self.logger.debug("device '%s' connection timed out" % dev.name) + else: + self.logger.debug("device '%s' had a connection error" % dev.name) + except ParseError: + # dev.setErrorStateOnServer('unavailable') + # self.logger.debug("device '%s' failed to update status with an XML parse error" % dev.name) + # This seems to happen relatively frequently - apparently sometimes the amp goes out to lunch for + # a bit (causing connection errors) and when it comes back it doesn't always return correct XML. + # I think the better idea here is to just pass on these errors as it always seems to resolve itself. + pass + except Exception as e: + dev.setErrorStateOnServer('unavailable') + self.logger.debug("device '%s' failed to update status with error: \n%s" % (dev.name, traceback.format_exc(10))) + + ############### + # _set_rxv_property + # + # Updates the specified property for the specified device. It catches exceptions and does the appropriate thing. + ############### + def _set_rxv_property(self, dev, property, value): + try: + d, rxv_obj = self.devices.get(dev.id, (None, None)) + if rxv_obj: + if callable(getattr(rxv_obj, property)): + if value: + getattr(rxv_obj, property)(value) + else: + getattr(rxv_obj, property)() + else: + setattr(rxv_obj, property, value) + else: + self.logger.error("device '%s' isn't available" % dev.name) + except (ConnectTimeout, ReadTimeout, ConnectionError) as e: + dev.setErrorStateOnServer('unavailable') + if isinstance(e, ConnectTimeout) or isinstance(e, ReadTimeout): + self.logger.debug("device '%s' connection timed out" % dev.name) + self.logger.error("device '%s' is unavailable" % dev.name) + else: + self.logger.debug("device '%s' had a connection error" % dev.name) + self.logger.error("device '%s' is unavailable" % dev.name) + except rxv_exceptions.ResponseException as e: + response = ET.XML(str(e)) + if response.get("RC") == "3": + # RC 3 is what happens when you issue a command that's not valid - wrong menu navigation direction, etc. + # We just skip it. + pass + elif response.get("RC") != "4": + self.logger.error("device '%s' can't have property '%s' set to value '%s'" % (dev.name, property, str(value))) + else: + # RC 4 is what happens when the amp is offline or in standby and you try to send it a command other than + # to turn on (if in standby) + self.logger.error("device '%s' is unavailable" % dev.name) + except ET.ParseError: + # dev.setErrorStateOnServer('unavailable') + # self.logger.debug("device '%s' failed to update status with an XML parse error" % dev.name) + # This seems to happen relatively frequently - apparently sometimes the amp goes out to lunch for + # a bit (causing connection errors) and when it comes back it doesn't always return correct XML. + # I think the better idea here is to just pass on these errors as it always seems to resolve itself. + pass + except: + dev.setErrorStateOnServer('unavailable') + self.logger.error("device '%s' failed to set property status with error: \n%s" % (dev.name, traceback.format_exc(10))) + + ################################## + # Action methods + ################################## + def getStatus(self, pluginAction, dev): + self.updateStatus(dev.id) + + def setMute(self, pluginAction, dev): + self.logger.debug(u"setMute called") + val = pluginAction.props['ddlmute'] + if dev.deviceTypeId == "receiver": + ClassicReceiver.putMute(self.logger, dev, val) + elif dev.deviceTypeId == "rxvX73": + self._set_rxv_property(dev, 'mute', str2bool(val)) + + def toggleMute(self, pluginAction, dev): + self.logger.debug(u"toggleMute called") + self.updateStatus(dev.id) + dev.refreshFromServer() + if dev.deviceTypeId == "receiver": + val = 'On' if dev.states['mute'] == 'Off' else 'Off' + ClassicReceiver.putMute(self.logger, dev, val) + elif dev.deviceTypeId == "rxvX73": + self._set_rxv_property(dev, 'mute', not str2bool(dev.states['mute'])) + + def setVolume(self, pluginAction, dev): + self.logger.debug(u"setVolume called") + val = pluginAction.props['txtvolume'] + if dev.deviceTypeId == "receiver": + ClassicReceiver.putVolume(self.logger, dev, val) + elif dev.deviceTypeId == "rxvX73": + self._set_rxv_property(dev, 'volume', float(val)) + + def increaseVolume(self, pluginAction, dev): + self.logger.debug(u"increaseVolume called") + self.updateStatus(dev.id) + dev.refreshFromServer() + val = float(dev.states['volume']) + int(pluginAction.props['txtincrement']) + if dev.deviceTypeId == "receiver": + ClassicReceiver.putVolume(self.logger, dev, int(val)) + elif dev.deviceTypeId == "rxvX73": + self._set_rxv_property(dev, 'volume', val) + + def decreaseVolume(self, pluginAction, dev): + self.logger.debug(u"decreaseVolume called") + self.updateStatus(dev.id) + dev.refreshFromServer() + val = float(dev.states['volume']) - int(pluginAction.props['txtincrement']) + if dev.deviceTypeId == "receiver": + ClassicReceiver.putVolume(self.logger, dev, int(val)) + elif dev.deviceTypeId == "rxvX73": + self._set_rxv_property(dev, 'volume', val) + + def setPower(self, pluginAction, dev): + self.logger.debug(u"setPower called") + val = pluginAction.props['ddlpower'] + if dev.deviceTypeId == "receiver": + ClassicReceiver.putPower(self.logger, dev, val) + elif dev.deviceTypeId == "rxvX73": + self._set_rxv_property(dev, 'on', str2bool(val)) + + def togglePower(self, pluginAction, dev): + self.logger.debug(u"togglePower called") + self.updateStatus(dev.id) + dev.refreshFromServer() + val = 'On' if (dev.states['power']=='Standby') else 'Standby' + if dev.deviceTypeId == "receiver": + ClassicReceiver.putPower(self.logger, dev, val) + elif dev.deviceTypeId == "rxvX73": + self._set_rxv_property(dev, 'on', str2bool(val)) + + def setSleep(self, pluginAction, dev): + self.logger.debug(u"setSleep called") + val = pluginAction.props['ddlsleep'].replace('n','') + if dev.deviceTypeId == "receiver": + ClassicReceiver.putSleep(self.logger, dev, val) + elif dev.deviceTypeId == "rxvX73": + self._set_rxv_property(dev, 'sleep', "Off" if val == "Off" else "%s min" % val) + + def setInput(self, pluginAction, dev): + self.logger.debug(u"setInput called") + if dev.deviceTypeId == "receiver": + val = pluginAction.props['ddlinput'].upper().replace(".","/").replace("_"," ") + ClassicReceiver.putInput(self.logger, dev, val) + elif dev.deviceTypeId == "rxvX73": + self._set_rxv_property(dev, 'input', pluginAction.props['ddlinput'].replace("_", " ").replace("iPod", "iPod (USB)")) + + def setZone(self, pluginAction, dev): + self.logger.debug(u"setZone called") + self._set_rxv_property(dev, 'zone', pluginAction.props['zone']) + + def playNetRadio(self, pluginAction, dev): + self.logger.debug(u"playNetRadio called") + self._set_rxv_property(dev, 'net_radio', pluginAction.props['path']) + + def menuUp(self, pluginAction, dev): + self.logger.debug(u"menuUp called") + self._set_rxv_property(dev, 'menu_up', None) + + def menuDown(self, pluginAction, dev): + self.logger.debug(u"menuDown called") + self._set_rxv_property(dev, 'menu_down', None) + + def menuLeft(self, pluginAction, dev): + self.logger.debug(u"menuLeft called") + self._set_rxv_property(dev, 'menu_left', None) + + def menuRight(self, pluginAction, dev): + self.logger.debug(u"menuRight called") + self._set_rxv_property(dev, 'menu_right', None) + + def menuSelect(self, pluginAction, dev): + self.logger.debug(u"menuSelect called") + self._set_rxv_property(dev, 'menu_sel', None) + + def menuReturn(self, pluginAction, dev): + self.logger.debug(u"menuReturn called") + self._set_rxv_property(dev, 'menu_return', None) \ No newline at end of file diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/.gitignore b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/.gitignore similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/.gitignore rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/.gitignore diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/__init__.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/__init__.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/__init__.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/__init__.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/ElementTree.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/ElementTree.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/ElementTree.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/ElementTree.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/__init__.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/__init__.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/__init__.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/__init__.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/cElementTree.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/cElementTree.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/cElementTree.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/cElementTree.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/common.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatbuilder.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatbuilder.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatbuilder.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatbuilder.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatreader.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatreader.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatreader.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/expatreader.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/lxml.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/lxml.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/lxml.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/lxml.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/minidom.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/minidom.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/minidom.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/minidom.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/pulldom.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/pulldom.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/pulldom.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/pulldom.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/sax.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/sax.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/sax.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/sax.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/xmlrpc.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/xmlrpc.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/xmlrpc.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/defusedxml/xmlrpc.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/exceptions.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/exceptions.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/exceptions.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/exceptions.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/rxv.py diff --git a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py similarity index 96% rename from Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py rename to YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py index 041d28b..8b8b518 100644 --- a/Yahama RX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py +++ b/YamahaRX.indigoPlugin/Contents/Server Plugin/rxv/ssdp.py @@ -10,11 +10,7 @@ import requests from .defusedxml import ElementTree -try: - from urllib.parse import urljoin -except ImportError: - from urlparse import urljoin - +from urllib.parse import urljoin SSDP_ADDR = '239.255.255.250' SSDP_PORT = 1900 diff --git a/Yahama RX.indigoPlugin/Contents/__init__.py b/YamahaRX.indigoPlugin/Contents/__init__.py similarity index 100% rename from Yahama RX.indigoPlugin/Contents/__init__.py rename to YamahaRX.indigoPlugin/Contents/__init__.py diff --git a/Yahama RX.indigoPlugin/__init__.py b/YamahaRX.indigoPlugin/__init__.py similarity index 100% rename from Yahama RX.indigoPlugin/__init__.py rename to YamahaRX.indigoPlugin/__init__.py