From b241b398c6798024a0ae31d9a37286fc2f108b13 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Mar 2018 20:17:34 +0200 Subject: [PATCH 001/170] Update README.md Fixed readme format --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 113907a..76ac923 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ Sony API lib. This is a python3 conversion from this project https://github.com/ It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant # Example -` +The example will load a json file with all data if it exists or connects to device and registers it, storing the json afterwards +``` stored_config = "bluray.json" device = None import os.path @@ -23,14 +24,14 @@ else: text_file.write(data) text_file.close() +# wake device is_on = device.get_power_status() if not is_on: - device.wakeonlan() -device.update_service_urls() -device.update_commands() + device.power(True) +# Play media device.play() -` +``` #Compatability List From 9985535d0e743b0d139c0a999b95a0e934840546 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Mar 2018 20:17:49 +0200 Subject: [PATCH 002/170] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 76ac923..2d8d0af 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ if not is_on: device.play() ``` -#Compatability List +# Compatability List LCD TV BRAVIA 2016 model or later: From e6685857642a4a5e66c3d3d0b8cd5f84701393cd Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Mar 2018 20:46:07 +0200 Subject: [PATCH 003/170] added pip config --- .gitignore | 1 + setup.cfg | 2 ++ setup.py | 22 +++++++++++----------- 3 files changed, 14 insertions(+), 11 deletions(-) create mode 100644 setup.cfg diff --git a/.gitignore b/.gitignore index eb5c12f..270dee3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Files used for testing bluray.json +.pypirc # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py index a9cf691..f144ed8 100644 --- a/setup.py +++ b/setup.py @@ -13,14 +13,14 @@ # make pythonpack # python setup.py register sdist upload # and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" -setup(name='braviarc', - version='0.1.0', - description=open(os.path.join(CURRENT_DIR, 'README.md')).read(), - install_requires=['requests'], - maintainer='Alexander Mohr', - maintainer_email='sonyapilib@mohr.io', - zip_safe=False, - packages=find_packages(), - include_package_data=True, - url='https://github.com/alexmohr/sonyapilib.git') -1 \ No newline at end of file +setup(name='sonyapilib', + packages = ['sonyapilib'], # this must be the same as the name above + version = '0.1', + description = 'Lib to control sony devices with theier soap api', + author = 'Alexander Mohr', + author_email = 'sonyapilib@mohr.io', + url = 'https://github.com/alexmohr/sonyapilib', # use the URL to the github repo + download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.1', + keywords = ['soap', 'sony', 'api'], # arbitrary keywords + classifiers = [], +) \ No newline at end of file From 715273de203e8cbac1d798a0d8cc58ea421836ad Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Mar 2018 21:09:42 +0200 Subject: [PATCH 004/170] renamed file to make imports better --- setup.py | 4 ++-- sonyapilib/{sonyApiLib.py => device.py} | 10 ++++++---- 2 files changed, 8 insertions(+), 6 deletions(-) rename sonyapilib/{sonyApiLib.py => device.py} (99%) diff --git a/setup.py b/setup.py index f144ed8..eab5a9d 100644 --- a/setup.py +++ b/setup.py @@ -15,12 +15,12 @@ # and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" setup(name='sonyapilib', packages = ['sonyapilib'], # this must be the same as the name above - version = '0.1', + version = '0.1.1', description = 'Lib to control sony devices with theier soap api', author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', url = 'https://github.com/alexmohr/sonyapilib', # use the URL to the github repo - download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.1', + download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.1.1', keywords = ['soap', 'sony', 'api'], # arbitrary keywords classifiers = [], ) \ No newline at end of file diff --git a/sonyapilib/sonyApiLib.py b/sonyapilib/device.py similarity index 99% rename from sonyapilib/sonyApiLib.py rename to sonyapilib/device.py index 46a13c9..4c20138 100644 --- a/sonyapilib/sonyApiLib.py +++ b/sonyapilib/device.py @@ -17,7 +17,7 @@ import jsonpickle -import ssdp +import sonyapilib.ssdp _LOGGER = logging.getLogger(__name__) TIMEOUT = 10 @@ -81,6 +81,9 @@ def __init__(self, host, port=50001, dmr_port=52323, ircc_location=None): self.ircc_url = "http://{0}:{1}/Ircc.xml".format(host, port) self.dmr_port = dmr_port + if len(self.actions) == 0: + self.update_service_urls() + @staticmethod def discover(): """ @@ -130,9 +133,9 @@ def wakeonlan(self): def update_service_urls(self): """ Initalizes the device by reading the necessary resources from it """ + lirc_url = urllib.parse.urlparse(self.ircc_url) - - + response = self.send_http(self.ircc_url, method=HttpMethod.GET) raw_data = response.text xml_data = xml.etree.ElementTree.fromstring(raw_data) @@ -613,4 +616,3 @@ def browserBookmarkList(self): def list(self): self.send_req_ircc(self.commands['List'].value) - From d17150f5621fe0bb5f256d16a8deab57f325d361 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Mar 2018 21:10:56 +0200 Subject: [PATCH 005/170] Update README.md --- README.md | 55 +++++++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 2d8d0af..f90e328 100644 --- a/README.md +++ b/README.md @@ -5,32 +5,35 @@ It may not contains all functionality which is implemented in the project from K # Example The example will load a json file with all data if it exists or connects to device and registers it, storing the json afterwards ``` -stored_config = "bluray.json" -device = None -import os.path -if os.path.exists(stored_config): - with open(stored_config, 'r') as content_file: - json_data = content_file.read() - device = SonyDevice.load_from_json(json_data) -else: - host = "10.0.0.102" - device = SonyDevice(host) - device.register("SonyApiLib Python Test") - pin = input("Enter the PIN displayed at your device: ") - device.send_authentication(pin) - - data = device.save_to_json() - text_file = open("bluray.json", "w") - text_file.write(data) - text_file.close() - -# wake device -is_on = device.get_power_status() -if not is_on: - device.power(True) - -# Play media -device.play() +from sonyapilib.device import SonyDevice +if __name__ == "__main__": + + stored_config = "bluray.json" + device = None + import os.path + if os.path.exists(stored_config): + with open(stored_config, 'r') as content_file: + json_data = content_file.read() + device = SonyDevice.load_from_json(json_data) + else: + host = "10.0.0.102" + device = SonyDevice(host) + device.register("SonyApiLib Python Test") + pin = input("Enter the PIN displayed at your device: ") + device.send_authentication(pin) + + data = device.save_to_json() + text_file = open("bluray.json", "w") + text_file.write(data) + text_file.close() + + # wake device + is_on = device.get_power_status() + if not is_on: + device.power(True) + + # Play media + device.play() ``` # Compatability List From 61a56518d4ec96aef9bac2ca88bc657063d52f48 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Mar 2018 21:14:24 +0200 Subject: [PATCH 006/170] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index f90e328..850a04e 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,11 @@ Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant +# Installation +``` +pip install sonyapilib +``` + # Example The example will load a json file with all data if it exists or connects to device and registers it, storing the json afterwards ``` From abbd2e9ce101fdbf30a8b15900a17474e46dd476 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 28 Mar 2018 20:07:58 +0200 Subject: [PATCH 007/170] made command hnadling more robust --- sonyapilib/device.py | 168 +++++++++++++++++++++++++++---------------- 1 file changed, 107 insertions(+), 61 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 4c20138..ed89728 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -84,6 +84,8 @@ def __init__(self, host, port=50001, dmr_port=52323, ircc_location=None): if len(self.actions) == 0: self.update_service_urls() + # http://10.0.0.102:50202/appslist + @staticmethod def discover(): """ @@ -458,161 +460,205 @@ def get_power_status(self): # def get_source(self, source): # pass - # def load_app_list(self, log_errors=True): - # pass + def load_app_list(self, log_errors=True): + # http://10.0.0.102:50202/appslist + pass - # def start_app(self, app_name, log_errors=True): - # """Start an app by name""" - # pass + def start_app(self, app_name, log_errors=True): + """Start an app by name""" + # post + + # http://10.0.0.102:50202/apps/Netflix + data = "LOCATION: http://10.0.0.102:50202/apps/com.sony.iptv.type.NRDP/run" + self.send_http("http://10.0.0.102:50202/apps/com.sony.iptv.type.NRDP", HttpMethod.POST, data=data) + pass + + def send_command(self, name): + if len(self.commands) == 0: + self.update_commands() + + self.send_req_ircc(self.commands[name].value) + def power(self, on): + if (on): + self.wakeonlan() + + # Try using the power on command incase the WOL doesn't work + if on and not self.get_power_status(): + self.send_command('Power') + def up(self): - self.send_req_ircc(self.commands['Up'].value) + self.send_command('Up') def confirm(self): - self.send_req_ircc(self.commands['Confirm'].value) + self.send_command('Confirm') def down(self): - self.send_req_ircc(self.commands['Down'].value) + self.send_command('Down') def right(self): - self.send_req_ircc(self.commands['Right'].value) + self.send_command('Right') def left(self): - self.send_req_ircc(self.commands['Left'].value) + self.send_command('Left') def home(self): - self.send_req_ircc(self.commands['Home'].value) + self.send_command('Home') def options(self): - self.send_req_ircc(self.commands['Options'].value) + self.send_command('Options') def returns(self): - self.send_req_ircc(self.commands['Return'].value) + self.send_command('Return') def num1(self): - self.send_req_ircc(self.commands['Num1'].value) + self.send_command('Num1') def num2(self): - self.send_req_ircc(self.commands['Num2'].value) + self.send_command('Num2') def num3(self): - self.send_req_ircc(self.commands['Num3'].value) + self.send_command('Num3') def num4(self): - self.send_req_ircc(self.commands['Num4'].value) + self.send_command('Num4') def num5(self): - self.send_req_ircc(self.commands['Num5'].value) + self.send_command('Num5') def num6(self): - self.send_req_ircc(self.commands['Num6'].value) + self.send_command('Num6') def num7(self): - self.send_req_ircc(self.commands['Num7'].value) + self.send_command('Num7') def num8(self): - self.send_req_ircc(self.commands['Num8'].value) + self.send_command('Num8') def num9(self): - self.send_req_ircc(self.commands['Num9'].value) + self.send_command('Num9') def num0(self): - self.send_req_ircc(self.commands['Num0'].value) - - def power(self, on): - if (on): - self.wakeonlan() - - # Try using the power on command incase the WOL doesn't work - if on and not self.get_power_status(): - self.send_req_ircc(self.commands['Power'].value) + self.send_command('Num0') def display(self): - self.send_req_ircc(self.commands['Display'].value) + self.send_command('Display') def audio(self): - self.send_req_ircc(self.commands['Audio'].value) + self.send_command('Audio') def subTitle(self): - self.send_req_ircc(self.commands['SubTitle'].value) + self.send_command('SubTitle') def favorites(self): - self.send_req_ircc(self.commands['Favorites'].value) + self.send_command('Favorites') def yellow(self): - self.send_req_ircc(self.commands['Yellow'].value) + self.send_command('Yellow') def blue(self): - self.send_req_ircc(self.commands['Blue'].value) + self.send_command('Blue') def red(self): - self.send_req_ircc(self.commands['Red'].value) + self.send_command('Red') def green(self): - self.send_req_ircc(self.commands['Green'].value) + self.send_command('Green') def play(self): - self.send_req_ircc(self.commands['Play'].value) + self.send_command('Play') def stop(self): - self.send_req_ircc(self.commands['Stop'].value) + self.send_command('Stop') def pause(self): - self.send_req_ircc(self.commands['Pause'].value) + self.send_command('Pause') def rewind(self): - self.send_req_ircc(self.commands['Rewind'].value) + self.send_command('Rewind') def forward(self): - self.send_req_ircc(self.commands['Forward'].value) + self.send_command('Forward') def prev(self): - self.send_req_ircc(self.commands['Prev'].value) + self.send_command('Prev') def next(self): - self.send_req_ircc(self.commands['Next'].value) + self.send_command('Next') def replay(self): - self.send_req_ircc(self.commands['Replay'].value) + self.send_command('Replay') def advance(self): - self.send_req_ircc(self.commands['Advance'].value) + self.send_command('Advance') def angle(self): - self.send_req_ircc(self.commands['Angle'].value) + self.send_command('Angle') def topMenu(self): - self.send_req_ircc(self.commands['TopMenu'].value) + self.send_command('TopMenu') def popUpMenu(self): - self.send_req_ircc(self.commands['PopUpMenu'].value) + self.send_command('PopUpMenu') def eject(self): - self.send_req_ircc(self.commands['Eject'].value) + self.send_command('Eject') def karaoke(self): - self.send_req_ircc(self.commands['Karaoke'].value) + self.send_command('Karaoke') def netflix(self): - self.send_req_ircc(self.commands['Netflix'].value) + self.send_command('Netflix') def mode3D(self): - self.send_req_ircc(self.commands['Mode3D'].value) + self.send_command('Mode3D') def zoomIn(self): - self.send_req_ircc(self.commands['ZoomIn'].value) + self.send_command('ZoomIn') def zoomOut(self): - self.send_req_ircc(self.commands['ZoomOut'].value) + self.send_command('ZoomOut') def browserBack(self): - self.send_req_ircc(self.commands['BrowserBack'].value) + self.send_command('BrowserBack') def browserForward(self): - self.send_req_ircc(self.commands['BrowserForward'].value) + self.send_command('BrowserForward') def browserBookmarkList(self): - self.send_req_ircc(self.commands['BrowserBookmarkList'].value) + self.send_command('BrowserBookmarkList') def list(self): - self.send_req_ircc(self.commands['List'].value) + self.send_command('List') + + +if __name__ == "__main__": + + stored_config = "bluray.json" + device = None + import os.path + if os.path.exists(stored_config): + with open(stored_config, 'r') as content_file: + json_data = content_file.read() + device = SonyDevice.load_from_json(json_data) + else: + host = "10.0.0.102" + device = SonyDevice(host) + device.register("SonyApiLib Python Test") + pin = input("Enter the PIN displayed at your device: ") + device.send_authentication(pin) + + data = device.save_to_json() + text_file = open("bluray.json", "w") + text_file.write(data) + text_file.close() + + # wake device + is_on = device.get_power_status() + if not is_on: + device.power(True) + + # device.update_commands() + device.start_app("fo") + # Play media + device.play() \ No newline at end of file From 738e2acb3b34ab101ca8d4d7d92872043529019b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 28 Mar 2018 20:51:54 +0200 Subject: [PATCH 008/170] Fixed issue with registration and added method to start apps. --- README.md | 28 ++++- setup.py | 4 +- sonyapilib/device.py | 281 +++++++++++++++++++++---------------------- 3 files changed, 163 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index 850a04e..339d410 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,15 @@ pip install sonyapilib The example will load a json file with all data if it exists or connects to device and registers it, storing the json afterwards ``` from sonyapilib.device import SonyDevice + + +def save_device(): + data = device.save_to_json() + text_file = open("bluray.json", "w") + text_file.write(data) + text_file.close() + + if __name__ == "__main__": stored_config = "bluray.json" @@ -21,24 +30,31 @@ if __name__ == "__main__": json_data = content_file.read() device = SonyDevice.load_from_json(json_data) else: + # device must be on for registration host = "10.0.0.102" device = SonyDevice(host) device.register("SonyApiLib Python Test") pin = input("Enter the PIN displayed at your device: ") device.send_authentication(pin) - - data = device.save_to_json() - text_file = open("bluray.json", "w") - text_file.write(data) - text_file.close() - + save_device() + # wake device is_on = device.get_power_status() if not is_on: device.power(True) + + apps = device.get_apps() + + device.start_app(apps[0]) + import time + time.sleep(5) + + device.start_app(apps[1]) # Play media device.play() + + ``` # Compatability List diff --git a/setup.py b/setup.py index eab5a9d..a97c164 100644 --- a/setup.py +++ b/setup.py @@ -15,12 +15,12 @@ # and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" setup(name='sonyapilib', packages = ['sonyapilib'], # this must be the same as the name above - version = '0.1.1', + version = '0.2', description = 'Lib to control sony devices with theier soap api', author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', url = 'https://github.com/alexmohr/sonyapilib', # use the URL to the github repo - download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.1.1', + download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.2', keywords = ['soap', 'sony', 'api'], # arbitrary keywords classifiers = [], ) \ No newline at end of file diff --git a/sonyapilib/device.py b/sonyapilib/device.py index ed89728..44bb808 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -17,7 +17,7 @@ import jsonpickle -import sonyapilib.ssdp +from sonyapilib import ssdp _LOGGER = logging.getLogger(__name__) TIMEOUT = 10 @@ -34,25 +34,27 @@ class HttpMethod(Enum): POST = 1 -class ApiAction(): +class XmlApiObject(): """ Holds data for a device action or a command """ def __init__(self, xml_data): - self.name = xml_data["name"] + self.name = None self.mode = None self.url = None self.type = None self.value = None self.mac = None + self.id = None - for arg in self.__dict__: - if "_" in arg: - continue - if arg in xml_data: - if (arg == "mode"): - setattr(self, arg, int(xml_data[arg])) - else: - setattr(self, arg, xml_data[arg]) + if xml_data is not None: + for arg in self.__dict__: + if "_" in arg: + continue + if arg in xml_data: + if (arg == "mode"): + setattr(self, arg, int(xml_data[arg])) + else: + setattr(self, arg, xml_data[arg]) class SonyDevice(): @@ -60,17 +62,21 @@ class SonyDevice(): Contains all data for the device """ - def __init__(self, host, port=50001, dmr_port=52323, ircc_location=None): + def __init__(self, host, port=50001, dmr_port=52323, app_port=50202, ircc_location=None): """ Init the device with the entry point""" self.host = host self.ircc_url = ircc_location self.actionlist_url = None self.control_url = None self.av_transport_url = None + self.app_url = None + + self.app_port = app_port self.actions = {} self.headers = {} self.commands = {} + self.apps = {} self.pin = None self.name = None @@ -79,9 +85,14 @@ def __init__(self, host, port=50001, dmr_port=52323, ircc_location=None): if self.ircc_url == None: self.ircc_url = "http://{0}:{1}/Ircc.xml".format(host, port) + self.dmr_port = dmr_port - if len(self.actions) == 0: + self.dmr_url = "http://{0}:{1}/dmr.xml".format( + self.host, self.dmr_port) + self.app_url = "http://{0}:{1}".format(self.host, self.app_port) + + if len(self.actions) == 0 and self.pin is not None: self.update_service_urls() # http://10.0.0.102:50202/appslist @@ -160,7 +171,7 @@ def update_service_urls(self): raw_data = response.text xml_data = xml.etree.ElementTree.fromstring(raw_data) for element in xml_data.findall("action"): - action = ApiAction(element.attrib) + action = XmlApiObject(element.attrib) self.actions[action.name] = action # overwrite urls for api version 4 to be consistent later. @@ -192,18 +203,62 @@ def update_service_urls(self): if function.attrib["name"] == "WOL": self.mac = function.find("functionItem").attrib["value"] - self.dmr_url = "{0}://{1}:{2}/dmr.xml".format(lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port) response = self.send_http(self.dmr_url, method=HttpMethod.GET) raw_data = response.text xml_data = xml.etree.ElementTree.fromstring(raw_data) for device in xml_data.findall("{0}device".format(urn_upnp_device)): serviceList = device.find("{0}serviceList".format(urn_upnp_device)) for service in serviceList: - service_id = service.find("{0}serviceId".format(urn_upnp_device)) + service_id = service.find( + "{0}serviceId".format(urn_upnp_device)) if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: continue - transport_location = service.find("{0}controlURL".format(urn_upnp_device)).text - self.av_transport_url ="{0}://{1}:{2}{3}".format(lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + transport_location = service.find( + "{0}controlURL".format(urn_upnp_device)).text + self.av_transport_url = "{0}://{1}:{2}{3}".format( + lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + + self.update_commands() + self.update_applist() + + def update_commands(self): + + # needs to be registred to do that + if self.pin is None: + return + + url = self.actions["getRemoteCommandList"].url + if self.actions["register"].mode < 4: + response = self.send_http(url, method=HttpMethod.GET) + xml_data = xml.etree.ElementTree.fromstring(response.text) + + for command in xml_data.findall("command"): + name = command.get("name") + self.commands[name] = XmlApiObject(command.attrib) + else: + response = self.send_http( + url, method=HttpMethod.POST, data=self.create_json_v4("getRemoteControllerInfo")) + json = response.json() + if not json.get('error'): + # todo this does not fit 100% with the structure of this lib. + # see github issue#2 + self.commands = json.get('result')[1] + else: + _LOGGER.error("JSON request error: " + + json.dumps(json, indent=4)) + + def update_applist(self, log_errors=True): + url = self.app_url + "/appslist" + response = self.send_http(url, method=HttpMethod.GET) + xml_data = xml.etree.ElementTree.fromstring(response.text) + apps = xml_data.findall(".//app") + for app in apps: + name = app.find("name").text + id = app.find("id").text + data = XmlApiObject(None) + data.name = name + data.id = id + self.apps[name] = data def recreate_auth_cookie(self): """ @@ -391,27 +446,6 @@ def send_http(self, url, method, data=None, headers=None, log_errors=True, raise else: return response - def update_commands(self): - url = self.actions["getRemoteCommandList"].url - if self.actions["register"].mode < 4: - response = self.send_http(url, method=HttpMethod.GET) - xml_data = xml.etree.ElementTree.fromstring(response.text) - - for command in xml_data.findall("command"): - name = command.get("name") - self.commands[name] = ApiAction(command.attrib) - else: - response = self.send_http( - url, method=HttpMethod.POST, data=self.create_json_v4("getRemoteControllerInfo")) - json = response.json() - if not json.get('error'): - # todo this does not fit 100% with the structure of this lib. - # see github issue#2 - self.commands = json.get('result')[1] - else: - _LOGGER.error("JSON request error: " + - json.dumps(json, indent=4)) - def post_soap_request(self, url, params, action): headers = { 'SOAPACTION': '"{0}"'.format(action), @@ -425,7 +459,7 @@ def post_soap_request(self, url, params, action): "" +\ "" return self.send_http(url, method=HttpMethod.POST, - headers=headers, data=data).content.decode("utf-8") + headers=headers, data=data).content.decode("utf-8") def send_req_ircc(self, params, log_errors=True): """Send an IRCC command via HTTP to Sony Bravia.""" @@ -435,230 +469,193 @@ def send_req_ircc(self, params, log_errors=True): "" action = "urn:schemas-sony-com:service:IRCC:1#X_SendIRCC" - content = self.post_soap_request(url=self.control_url, params=data, action=action) + content = self.post_soap_request( + url=self.control_url, params=data, action=action) return content - def send_command(self, command): - """Sends a command to the Device.""" - if not self.commands: - self.update_commands() - def get_playing_info(self): # the device which i got for testing only deliviers default values # therefore not implemented pass - def get_power_status(self): url = self.ircc_url try: - self.send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) - except: + self.send_http(url, HttpMethod.GET, + log_errors=False, raise_errors=True) + except: return False return True # def get_source(self, source): # pass - def load_app_list(self, log_errors=True): - # http://10.0.0.102:50202/appslist - pass - def start_app(self, app_name, log_errors=True): """Start an app by name""" - # post - - # http://10.0.0.102:50202/apps/Netflix - data = "LOCATION: http://10.0.0.102:50202/apps/com.sony.iptv.type.NRDP/run" - self.send_http("http://10.0.0.102:50202/apps/com.sony.iptv.type.NRDP", HttpMethod.POST, data=data) + # sometimes device does not start app if already running one + self.home() + url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) + data = "LOCATION: {0}/run".format(url) + self.send_http(url, HttpMethod.POST, data=data) pass def send_command(self, name): - if len(self.commands) == 0: + if len(self.commands) == 0: self.update_commands() - + self.send_req_ircc(self.commands[name].value) def power(self, on): - if (on): + if (on): self.wakeonlan() - + # Try using the power on command incase the WOL doesn't work if on and not self.get_power_status(): self.send_command('Power') - + + def get_apps(self): + return list(self.apps.keys()) + def up(self): self.send_command('Up') - + def confirm(self): self.send_command('Confirm') - + def down(self): self.send_command('Down') - + def right(self): self.send_command('Right') - + def left(self): self.send_command('Left') - + def home(self): self.send_command('Home') - + def options(self): self.send_command('Options') - + def returns(self): self.send_command('Return') - + def num1(self): self.send_command('Num1') - + def num2(self): self.send_command('Num2') - + def num3(self): self.send_command('Num3') - + def num4(self): self.send_command('Num4') - + def num5(self): self.send_command('Num5') - + def num6(self): self.send_command('Num6') - + def num7(self): self.send_command('Num7') - + def num8(self): self.send_command('Num8') - + def num9(self): self.send_command('Num9') - + def num0(self): self.send_command('Num0') - + def display(self): self.send_command('Display') - + def audio(self): self.send_command('Audio') - + def subTitle(self): self.send_command('SubTitle') - + def favorites(self): self.send_command('Favorites') - + def yellow(self): self.send_command('Yellow') - + def blue(self): self.send_command('Blue') - + def red(self): self.send_command('Red') - + def green(self): self.send_command('Green') - + def play(self): self.send_command('Play') - + def stop(self): self.send_command('Stop') - + def pause(self): self.send_command('Pause') - + def rewind(self): self.send_command('Rewind') - + def forward(self): self.send_command('Forward') - + def prev(self): self.send_command('Prev') - + def next(self): self.send_command('Next') - + def replay(self): self.send_command('Replay') - + def advance(self): self.send_command('Advance') - + def angle(self): self.send_command('Angle') - + def topMenu(self): self.send_command('TopMenu') - + def popUpMenu(self): self.send_command('PopUpMenu') - + def eject(self): self.send_command('Eject') - + def karaoke(self): self.send_command('Karaoke') - + def netflix(self): self.send_command('Netflix') - + def mode3D(self): self.send_command('Mode3D') - + def zoomIn(self): self.send_command('ZoomIn') - + def zoomOut(self): self.send_command('ZoomOut') - + def browserBack(self): self.send_command('BrowserBack') - + def browserForward(self): self.send_command('BrowserForward') - + def browserBookmarkList(self): self.send_command('BrowserBookmarkList') - + def list(self): self.send_command('List') - - -if __name__ == "__main__": - - stored_config = "bluray.json" - device = None - import os.path - if os.path.exists(stored_config): - with open(stored_config, 'r') as content_file: - json_data = content_file.read() - device = SonyDevice.load_from_json(json_data) - else: - host = "10.0.0.102" - device = SonyDevice(host) - device.register("SonyApiLib Python Test") - pin = input("Enter the PIN displayed at your device: ") - device.send_authentication(pin) - - data = device.save_to_json() - text_file = open("bluray.json", "w") - text_file.write(data) - text_file.close() - - # wake device - is_on = device.get_power_status() - if not is_on: - device.power(True) - - # device.update_commands() - device.start_app("fo") - # Play media - device.play() \ No newline at end of file From 9df445baf9abd8ca3ab5af2de21fec3b488d664f Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 31 Mar 2018 21:00:53 +0200 Subject: [PATCH 009/170] Fixed a lot of issues in combination with home assistant. Improved stability --- .gitignore | 1 + README.md | 13 +- requirements.txt | 1 - setup.py | 10 +- sonyapilib/device.py | 288 +++++++++++++++++++++++++------------------ 5 files changed, 179 insertions(+), 134 deletions(-) delete mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 270dee3..1d6d2bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Files used for testing bluray.json +cp.sh .pypirc diff --git a/README.md b/README.md index 339d410..331b93c 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,12 @@ The example will load a json file with all data if it exists or connects to devi ``` from sonyapilib.device import SonyDevice - def save_device(): data = device.save_to_json() text_file = open("bluray.json", "w") text_file.write(data) text_file.close() - if __name__ == "__main__": stored_config = "bluray.json" @@ -32,8 +30,7 @@ if __name__ == "__main__": else: # device must be on for registration host = "10.0.0.102" - device = SonyDevice(host) - device.register("SonyApiLib Python Test") + device = SonyDevice(host, "SonyApiLib Python Test") pin = input("Enter the PIN displayed at your device: ") device.send_authentication(pin) save_device() @@ -43,18 +40,12 @@ if __name__ == "__main__": if not is_on: device.power(True) - apps = device.get_apps() device.start_app(apps[0]) - import time - time.sleep(5) - - device.start_app(apps[1]) + # Play media device.play() - - ``` # Compatability List diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 60a7ea6..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -jsonpickle \ No newline at end of file diff --git a/setup.py b/setup.py index a97c164..401355d 100644 --- a/setup.py +++ b/setup.py @@ -15,12 +15,18 @@ # and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" setup(name='sonyapilib', packages = ['sonyapilib'], # this must be the same as the name above - version = '0.2', + version = '0.3', description = 'Lib to control sony devices with theier soap api', author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', url = 'https://github.com/alexmohr/sonyapilib', # use the URL to the github repo - download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.2', + download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3', keywords = ['soap', 'sony', 'api'], # arbitrary keywords classifiers = [], + install_requires=[ + 'jsonpickle', + 'setuptools', + 'requests' + ], + ) \ No newline at end of file diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 44bb808..ef7920b 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -20,7 +20,7 @@ from sonyapilib import ssdp _LOGGER = logging.getLogger(__name__) -TIMEOUT = 10 +TIMEOUT = 50 class AuthenicationResult(Enum): @@ -54,17 +54,17 @@ def __init__(self, xml_data): if (arg == "mode"): setattr(self, arg, int(xml_data[arg])) else: - setattr(self, arg, xml_data[arg]) - + setattr(self, arg, xml_data[arg]) class SonyDevice(): """ Contains all data for the device """ - def __init__(self, host, port=50001, dmr_port=52323, app_port=50202, ircc_location=None): + def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, ircc_location=None): """ Init the device with the entry point""" self.host = host + self.nickname = nickname self.ircc_url = ircc_location self.actionlist_url = None self.control_url = None @@ -82,6 +82,7 @@ def __init__(self, host, port=50001, dmr_port=52323, app_port=50202, ircc_locati self.name = None self.cookies = None self.mac = None + self.authenticated = False if self.ircc_url == None: self.ircc_url = "http://{0}:{1}/Ircc.xml".format(host, port) @@ -94,9 +95,7 @@ def __init__(self, host, port=50001, dmr_port=52323, app_port=50202, ircc_locati if len(self.actions) == 0 and self.pin is not None: self.update_service_urls() - - # http://10.0.0.102:50202/appslist - + @staticmethod def discover(): """ @@ -116,6 +115,7 @@ def load_from_json(data): return jsonpickle.decode(data) def save_to_json(self): + return jsonpickle.dumps(self) def create_json_v4(self, method, params=None): @@ -148,8 +148,9 @@ def update_service_urls(self): """ Initalizes the device by reading the necessary resources from it """ lirc_url = urllib.parse.urlparse(self.ircc_url) - response = self.send_http(self.ircc_url, method=HttpMethod.GET) + if response is None: + return raw_data = response.text xml_data = xml.etree.ElementTree.fromstring(raw_data) @@ -168,58 +169,78 @@ def update_service_urls(self): # read action list response = self.send_http(self.actionlist_url, method=HttpMethod.GET) - raw_data = response.text - xml_data = xml.etree.ElementTree.fromstring(raw_data) - for element in xml_data.findall("action"): - action = XmlApiObject(element.attrib) - self.actions[action.name] = action - - # overwrite urls for api version 4 to be consistent later. - # todo check if this is necessary - # if self.actions["register"].mode == 4: - # self.actions["getRemoteCommandList"].url = "http://{0}/sony/system".format( - # lirc_url.netloc.split(":")[0]) - - # read service list - for service in services: - service_id = service.find("{0}serviceId".format(urn_upnp_device)) - if service_id == None: - continue - if "urn:schemas-sony-com:serviceId:IRCC" not in service_id.text: - continue - - service_location = service.find( - "{0}controlURL".format(urn_upnp_device)).text - service_url = lirc_url.scheme + "://" + lirc_url.netloc - self.control_url = service_url + service_location + if response is not None: + raw_data = response.text + xml_data = xml.etree.ElementTree.fromstring(raw_data) + for element in xml_data.findall("action"): + action = XmlApiObject(element.attrib) + self.actions[action.name] = action + + # some data has to overwritten for the registration to work properly + if action.name == "register": + if action.mode < 4: + # the authenication later on is based on the device id and the mac + action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format( + action.url, urllib.parse.quote(self.nickname)) + if action.mode == 3: + action.url = action.url + "&wolSupport=true" + elif action.mode == 4: + pass + # overwrite urls for api version 4 to be consistent later. + # todo check if this is necessary + # if self.actions["register"].mode == 4: + # self.actions["getRemoteCommandList"].url = "http://{0}/sony/system".format( + # lirc_url.netloc.split(":")[0]) + + # make sure we are authenticated before + self.recreate_authentication() + + if services is not None: + # read service list + for service in services: + service_id = service.find("{0}serviceId".format(urn_upnp_device)) + if service_id == None: + continue + if "urn:schemas-sony-com:serviceId:IRCC" not in service_id.text: + continue + + service_location = service.find( + "{0}controlURL".format(urn_upnp_device)).text + service_url = lirc_url.scheme + "://" + lirc_url.netloc + self.control_url = service_url + service_location # get systeminformation response = self.send_http( - self.actions["getSystemInformation"].url, method=HttpMethod.GET) - raw_data = response.text - xml_data = xml.etree.ElementTree.fromstring(raw_data) - for element in xml_data.findall("supportFunction"): - for function in element.findall("function"): - if function.attrib["name"] == "WOL": - self.mac = function.find("functionItem").attrib["value"] + self.get_action("getSystemInformation").url, method=HttpMethod.GET) - response = self.send_http(self.dmr_url, method=HttpMethod.GET) - raw_data = response.text - xml_data = xml.etree.ElementTree.fromstring(raw_data) - for device in xml_data.findall("{0}device".format(urn_upnp_device)): - serviceList = device.find("{0}serviceList".format(urn_upnp_device)) - for service in serviceList: - service_id = service.find( - "{0}serviceId".format(urn_upnp_device)) - if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: - continue - transport_location = service.find( - "{0}controlURL".format(urn_upnp_device)).text - self.av_transport_url = "{0}://{1}:{2}{3}".format( - lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + if response is not None: + raw_data = response.text + xml_data = xml.etree.ElementTree.fromstring(raw_data) + for element in xml_data.findall("supportFunction"): + for function in element.findall("function"): + if function.attrib["name"] == "WOL": + self.mac = function.find("functionItem").attrib["value"] - self.update_commands() - self.update_applist() + # get control data for sending commands + response = self.send_http(self.dmr_url, method=HttpMethod.GET) + if response is not None: + raw_data = response.text + xml_data = xml.etree.ElementTree.fromstring(raw_data) + for device in xml_data.findall("{0}device".format(urn_upnp_device)): + serviceList = device.find("{0}serviceList".format(urn_upnp_device)) + for service in serviceList: + service_id = service.find( + "{0}serviceId".format(urn_upnp_device)) + if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: + continue + transport_location = service.find( + "{0}controlURL".format(urn_upnp_device)).text + self.av_transport_url = "{0}://{1}:{2}{3}".format( + lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + + if len(self.commands) > 0: + self.update_commands() + self.update_applist() def update_commands(self): @@ -227,72 +248,79 @@ def update_commands(self): if self.pin is None: return - url = self.actions["getRemoteCommandList"].url - if self.actions["register"].mode < 4: + url = self.get_action("getRemoteCommandList").url + if self.get_action("register").mode < 4: response = self.send_http(url, method=HttpMethod.GET) - xml_data = xml.etree.ElementTree.fromstring(response.text) + if response is not None: + xml_data = xml.etree.ElementTree.fromstring(response.text) - for command in xml_data.findall("command"): - name = command.get("name") - self.commands[name] = XmlApiObject(command.attrib) + for command in xml_data.findall("command"): + name = command.get("name") + self.commands[name] = XmlApiObject(command.attrib) else: response = self.send_http( url, method=HttpMethod.POST, data=self.create_json_v4("getRemoteControllerInfo")) - json = response.json() - if not json.get('error'): - # todo this does not fit 100% with the structure of this lib. - # see github issue#2 - self.commands = json.get('result')[1] - else: - _LOGGER.error("JSON request error: " + - json.dumps(json, indent=4)) + if response is not None: + json = response.json() + if not json.get('error'): + # todo this does not fit 100% with the structure of this lib. + # see github issue#2 + self.commands = json.get('result')[1] + else: + _LOGGER.error("JSON request error: " + + json.dumps(json, indent=4)) def update_applist(self, log_errors=True): url = self.app_url + "/appslist" response = self.send_http(url, method=HttpMethod.GET) - xml_data = xml.etree.ElementTree.fromstring(response.text) - apps = xml_data.findall(".//app") - for app in apps: - name = app.find("name").text - id = app.find("id").text - data = XmlApiObject(None) - data.name = name - data.id = id - self.apps[name] = data - - def recreate_auth_cookie(self): + if response is not None: + xml_data = xml.etree.ElementTree.fromstring(response.text) + apps = xml_data.findall(".//app") + for app in apps: + name = app.find("name").text + id = app.find("id").text + data = XmlApiObject(None) + data.name = name + data.id = id + self.apps[name] = data + + def recreate_authentication(self): """ The default cookie is for URL/sony. For some commands we need it for the root path. Only for api v4 """ - cookies = requests.cookies.RequestsCookieJar() - cookies.set("auth", self.cookies.get("auth")) + + if self.pin == None: + return + + # todo fix cookies + cookies = None + #cookies = requests.cookies.RequestsCookieJar() + #cookies.set("auth", self.cookies.get("auth")) + + username = '' + base64string = base64.encodebytes(('%s:%s' % (username, self.pin)).encode()) \ + .decode().replace('\n', '') + + registration_action = self.get_action("register") + + self.headers['Authorization'] = "Basic %s" % base64string + if registration_action.mode == 3: + self.headers['X-CERS-DEVICE-ID'] = self.nickname + elif registration_action.mode == 4: + self.headers['Connection'] = "keep-alive" + return cookies - def register(self, name): + def register(self): """ Register at the api.50001 :param str name: The name which will be displayed in the UI of the device. Make sure this name does not exist yet For this the device must be put in registration mode. The tested sd5500 has no separte mode but allows registration in the overview " """ - - if not "register" in self.actions: - self.update_service_urls() - - self.name = name registrataion_result = AuthenicationResult.ERROR - registration_action = self.actions["register"] - - if registration_action.mode < 4: - # the authenication later on is based on the device id and the mac - # address of the device - registration_action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format( - registration_action.url, urllib.parse.quote(name)) - if registration_action.mode == 3: - registration_action.url = registration_action.url + "&wolSupport=true" - else: - registration_action.url = registration_action.url + registration_action = registration_action = self.get_action("register") # protocoll version 1 and 2 if registration_action.mode < 3: @@ -317,8 +345,8 @@ def register(self, name): authorization = json.dumps( { "method": "actRegister", - "params": [{"clientid": name, - "nickname": name, + "params": [{"clientid": self.nickname, + "nickname": self.nickname, "level": "private"}, [{"value": "yes", "function": "WOL"}]], @@ -349,19 +377,15 @@ def register(self, name): return registrataion_result def send_authentication(self, pin): - registration_action = self.actions["register"] + registration_action = self.get_action("register") - username = '' - base64string = base64.encodebytes(('%s:%s' % (username, pin)).encode()) \ - .decode().replace('\n', '') - self.headers['Authorization'] = "Basic %s" % base64string + self.pin = pin + self.recreate_authentication() if registration_action.mode == 3: - self.headers['X-CERS-DEVICE-ID'] = self.name - try: self.send_http( - self.actions["register"].url, method=HttpMethod.GET, raise_errors=True) + self.get_action("register").url, method=HttpMethod.GET, raise_errors=True) except: return False else: @@ -370,8 +394,6 @@ def send_authentication(self, pin): return False elif registration_action.mode == 4: - self.headers['Connection'] = "keep-alive" - authorization = json.dumps( { "id": 13, @@ -395,7 +417,7 @@ def send_authentication(self, pin): ) try: - response = self.send_http(self.actions["register"].url, method=HttpMethod.post, + response = self.send_http(self.get_action("register").url, method=HttpMethod.post, data=authorization, raise_errors=True) except: return False @@ -414,6 +436,11 @@ def send_http(self, url, method, data=None, headers=None, log_errors=True, raise if headers is None: headers = self.headers + if url is None: + return + + _LOGGER.debug("Calling http url {0} method {1}".format(url, str(method))) + try: params = "" if data is not None: @@ -473,17 +500,27 @@ def send_req_ircc(self, params, log_errors=True): url=self.control_url, params=data, action=action) return content - def get_playing_info(self): - # the device which i got for testing only deliviers default values - # therefore not implemented - pass + def get_playing_status(self): + data = '' + \ + '0' + \ + '' + + action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" + + + content = self.post_soap_request( + url=self.av_transport_url, params=data, action=action) + response = xml.etree.ElementTree.fromstring(content) + state = response.find(".//CurrentTransportState").text + return state def get_power_status(self): - url = self.ircc_url + url = self.actionlist_url try: self.send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) - except: + except Exception as ex: + _LOGGER.debug(ex) return False return True @@ -505,13 +542,24 @@ def send_command(self, name): self.send_req_ircc(self.commands[name].value) + def get_action(self, name): + if not name in self.actions and len(self.actions) == 0: + self.update_service_urls() + if not name in self.actions and len(self.actions) == 0: + raise ValueError('Failed to read action list from device.') + + return self.actions[name] + def power(self, on): if (on): self.wakeonlan() + if not self.get_power_status(): + self.send_command('Power') + else: + self.send_command('Power') # Try using the power on command incase the WOL doesn't work - if on and not self.get_power_status(): - self.send_command('Power') + def get_apps(self): return list(self.apps.keys()) From ee6fa0f234a5505d40479ce0ec632f5399cf52e1 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 31 Mar 2018 21:21:54 +0200 Subject: [PATCH 010/170] More minor fixes to prevent uneccssary logging. --- setup.py | 2 +- sonyapilib/device.py | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 401355d..6ad7ad3 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', url = 'https://github.com/alexmohr/sonyapilib', # use the URL to the github repo - download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3', + download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3.7', keywords = ['soap', 'sony', 'api'], # arbitrary keywords classifiers = [], install_requires=[ diff --git a/sonyapilib/device.py b/sonyapilib/device.py index ef7920b..e3aa84e 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -485,8 +485,9 @@ def post_soap_request(self, url, params, action): params +\ "" +\ "" - return self.send_http(url, method=HttpMethod.POST, - headers=headers, data=data).content.decode("utf-8") + response = self.send_http(url, method=HttpMethod.POST,headers=headers, data=data) + if response is not None: + return response.content.decode("utf-8") def send_req_ircc(self, params, log_errors=True): """Send an IRCC command via HTTP to Sony Bravia.""" @@ -507,9 +508,10 @@ def get_playing_status(self): action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" - content = self.post_soap_request( url=self.av_transport_url, params=data, action=action) + if None is content: + return "OFF" response = xml.etree.ElementTree.fromstring(content) state = response.find(".//CurrentTransportState").text return state @@ -517,7 +519,7 @@ def get_playing_status(self): def get_power_status(self): url = self.actionlist_url try: - self.send_http(url, HttpMethod.GET, + responst = self.send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) except Exception as ex: _LOGGER.debug(ex) From 00e8ebc5cf169a8ed9a57305c2fb0980261d5b28 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 31 Mar 2018 21:22:33 +0200 Subject: [PATCH 011/170] version bump --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 6ad7ad3..523f537 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ # and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" setup(name='sonyapilib', packages = ['sonyapilib'], # this must be the same as the name above - version = '0.3', + version = '0.3.7', description = 'Lib to control sony devices with theier soap api', author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', From e658cba2df8823d06b5c7f65c77da7e4987e4bc9 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 31 Mar 2018 21:28:26 +0200 Subject: [PATCH 012/170] reduced timeout --- sonyapilib/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index e3aa84e..661d071 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -20,7 +20,7 @@ from sonyapilib import ssdp _LOGGER = logging.getLogger(__name__) -TIMEOUT = 50 +TIMEOUT = 5 class AuthenicationResult(Enum): From d1d4595f16506c79ce0d8e7e86e97daf2fec7c29 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 31 Mar 2018 21:28:58 +0200 Subject: [PATCH 013/170] reduced timeout --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 523f537..b91b43b 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ # and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" setup(name='sonyapilib', packages = ['sonyapilib'], # this must be the same as the name above - version = '0.3.7', + version = '0.3.8', description = 'Lib to control sony devices with theier soap api', author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', From 4c5f7e42b43dbb1a6a5b32406dd2d0e4e9243583 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 May 2018 19:08:45 +0200 Subject: [PATCH 014/170] Added file to convert into hassbian configuration --- convert_to_hass.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 convert_to_hass.py diff --git a/convert_to_hass.py b/convert_to_hass.py new file mode 100644 index 0000000..2570500 --- /dev/null +++ b/convert_to_hass.py @@ -0,0 +1,18 @@ +import json +from sonyapilib.device import SonyDevice + +config_file = 'bluray.json' + + + + + +with open(config_file, 'r') as myfile: + data=myfile.read() + +device = SonyDevice.load_from_json(data) + +hass_cfg = {} +hass_cfg[device.host] = {} +hass_cfg[device.host]["device"] = data +print(json.dumps(hass_cfg), file=open("sony.conf", "w")) \ No newline at end of file From 82cfa808fc45b6f1eb09de093421457f3e043593 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 21 May 2018 21:24:49 +0200 Subject: [PATCH 015/170] fix for version < 3 devices. --- sonyapilib/device.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 661d071..33a8c6c 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -377,8 +377,13 @@ def register(self): return registrataion_result def send_authentication(self, pin): + registration_action = self.get_action("register") + # they do not need a pin + if registration_action.mode < 3: + return True + self.pin = pin self.recreate_authentication() @@ -519,7 +524,7 @@ def get_playing_status(self): def get_power_status(self): url = self.actionlist_url try: - responst = self.send_http(url, HttpMethod.GET, + response = self.send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) except Exception as ex: _LOGGER.debug(ex) From b3cffd78673aebca53cddac71ef1b6484672cb35 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 22 May 2018 19:43:26 +0200 Subject: [PATCH 016/170] Added script for services discovery --- discover_devices.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 discover_devices.py diff --git a/discover_devices.py b/discover_devices.py new file mode 100644 index 0000000..5e01e30 --- /dev/null +++ b/discover_devices.py @@ -0,0 +1,9 @@ +import json +from sonyapilib.ssdp import SSDPDiscovery + +ip = "10.0.0.102" +ssdp = SSDPDiscovery() +services = ssdp.discover() +for service in services: + if ip in str(service): + print(service) From 47b024658d46b9dc1eb21ae04996f6e58001ae46 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 22 May 2018 19:51:41 +0200 Subject: [PATCH 017/170] fixed some issues with version < 3 --- convert_to_hass.py | 5 +---- setup.py | 2 +- sonyapilib/device.py | 7 ++----- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/convert_to_hass.py b/convert_to_hass.py index 2570500..f7ffab6 100644 --- a/convert_to_hass.py +++ b/convert_to_hass.py @@ -1,11 +1,8 @@ import json from sonyapilib.device import SonyDevice -config_file = 'bluray.json' - - - +config_file = 'bluray.json' with open(config_file, 'r') as myfile: data=myfile.read() diff --git a/setup.py b/setup.py index b91b43b..65d2171 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ # and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" setup(name='sonyapilib', packages = ['sonyapilib'], # this must be the same as the name above - version = '0.3.8', + version = '0.3.10', description = 'Lib to control sony devices with theier soap api', author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 33a8c6c..1c915b6 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -324,12 +324,9 @@ def register(self): # protocoll version 1 and 2 if registration_action.mode < 3: - registration_response = self.send_http( + self.send_http( registration_action.url, method=HttpMethod.GET, raise_errors=True) - if registration_response.text == "": - registrataion_result = AuthenicationResult.SUCCESS - else: - registrataion_result = AuthenicationResult.ERROR + registrataion_result = AuthenicationResult.SUCCESS # protocoll version 3 elif registration_action.mode == 3: From e1159a1ef1ac1ab4db41005f48cedd9c47363f7e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Mar 2019 05:27:18 +0100 Subject: [PATCH 018/170] Update README.md added call to register in example code. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 331b93c..aad28e0 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ if __name__ == "__main__": # device must be on for registration host = "10.0.0.102" device = SonyDevice(host, "SonyApiLib Python Test") + device.register() pin = input("Enter the PIN displayed at your device: ") device.send_authentication(pin) save_device() From dd519f331a1b1c4377f77429dad8b3bac066f376 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Mar 2019 09:02:00 +0100 Subject: [PATCH 019/170] Fixed registration url for gen2 devices --- sonyapilib/device.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 661d071..0df762a 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -23,7 +23,7 @@ TIMEOUT = 5 -class AuthenicationResult(Enum): +class AuthenticationResult(Enum): SUCCESS = 0 ERROR = 1 PIN_NEEDED = 2 @@ -179,9 +179,12 @@ def update_service_urls(self): # some data has to overwritten for the registration to work properly if action.name == "register": if action.mode < 4: + if action.mode == 1 or action.mode == 3: + action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format(action.url, urllib.parse.quote(self.nickname)) + elif action.mode == 2: + action.url = "{0}?name={1}®istrationType=new&deviceId={1}".format(action.url, urllib.parse.quote(self.nickname)) + # the authenication later on is based on the device id and the mac - action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format( - action.url, urllib.parse.quote(self.nickname)) if action.mode == 3: action.url = action.url + "&wolSupport=true" elif action.mode == 4: @@ -317,30 +320,29 @@ def register(self): Register at the api.50001 :param str name: The name which will be displayed in the UI of the device. Make sure this name does not exist yet For this the device must be put in registration mode. - The tested sd5500 has no separte mode but allows registration in the overview " """ - registrataion_result = AuthenicationResult.ERROR + registration_result = AuthenticationResult.ERROR registration_action = registration_action = self.get_action("register") - # protocoll version 1 and 2 + # protocol version 1 and 2 if registration_action.mode < 3: registration_response = self.send_http( registration_action.url, method=HttpMethod.GET, raise_errors=True) if registration_response.text == "": - registrataion_result = AuthenicationResult.SUCCESS + registration_result = AuthenticationResult.SUCCESS else: - registrataion_result = AuthenicationResult.ERROR + registration_result = AuthenticationResult.ERROR - # protocoll version 3 + # protocol version 3 elif registration_action.mode == 3: try: self.send_http(registration_action.url, method=HttpMethod.GET, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registrataion_result = AuthenicationResult.PIN_NEEDED + registration_result = AuthenticationResult.PIN_NEEDED - # newest protocoll version 4 this is the same method as braviarc uses + # newest protocol version 4 this is the same method as braviarc uses elif registration_action.mode == 4: authorization = json.dumps( { @@ -359,7 +361,7 @@ def register(self): data=authorization, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registrataion_result = AuthenicationResult.PIN_NEEDED + registration_result = AuthenticationResult.PIN_NEEDED except Exception as ex: # pylint: disable=broad-except _LOGGER.error("[W] Exception: " + str(ex)) @@ -368,13 +370,13 @@ def register(self): _LOGGER.debug(json.dumps(resp, indent=4)) if resp is None or not resp.get('error'): self.cookies = response.cookies - registrataion_result = AuthenicationResult.SUCCESS + registration_result = AuthenticationResult.SUCCESS else: raise ValueError( "Regisration mode {0} is not supported".format(registration_action.mode)) - return registrataion_result + return registration_result def send_authentication(self, pin): registration_action = self.get_action("register") From ca631e9a24d470ccdd50a1c58784df1fa85b09c9 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Mar 2019 08:55:03 +0100 Subject: [PATCH 020/170] starting impl of API version 4 --- discover_devices.py | 1 - example.py | 7 ++ sonyapilib/device.py | 214 +++++++++++++++++++++++-------------------- 3 files changed, 120 insertions(+), 102 deletions(-) create mode 100644 example.py diff --git a/discover_devices.py b/discover_devices.py index 5e01e30..cdb33fb 100644 --- a/discover_devices.py +++ b/discover_devices.py @@ -5,5 +5,4 @@ ssdp = SSDPDiscovery() services = ssdp.discover() for service in services: - if ip in str(service): print(service) diff --git a/example.py b/example.py new file mode 100644 index 0000000..060d468 --- /dev/null +++ b/example.py @@ -0,0 +1,7 @@ +from sonyapilib.device import SonyDevice + +def save_device(): + data = device.save_to_json() + text_file = open("bluray.json", "w") + text_file.write(data) + text_file.close() diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 1c915b6..e11ecb7 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -54,7 +54,8 @@ def __init__(self, xml_data): if (arg == "mode"): setattr(self, arg, int(xml_data[arg])) else: - setattr(self, arg, xml_data[arg]) + setattr(self, arg, xml_data[arg]) + class SonyDevice(): """ @@ -95,7 +96,7 @@ def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, i if len(self.actions) == 0 and self.pin is not None: self.update_service_urls() - + @staticmethod def discover(): """ @@ -115,7 +116,7 @@ def load_from_json(data): return jsonpickle.decode(data) def save_to_json(self): - + return jsonpickle.dumps(self) def create_json_v4(self, method, params=None): @@ -175,30 +176,28 @@ def update_service_urls(self): for element in xml_data.findall("action"): action = XmlApiObject(element.attrib) self.actions[action.name] = action - + # some data has to overwritten for the registration to work properly if action.name == "register": if action.mode < 4: - # the authenication later on is based on the device id and the mac + # the authentication later on is based on the device id and the mac action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format( action.url, urllib.parse.quote(self.nickname)) if action.mode == 3: action.url = action.url + "&wolSupport=true" elif action.mode == 4: - pass # overwrite urls for api version 4 to be consistent later. - # todo check if this is necessary - # if self.actions["register"].mode == 4: - # self.actions["getRemoteCommandList"].url = "http://{0}/sony/system".format( - # lirc_url.netloc.split(":")[0]) + if self.actions["register"].mode == 4: + self.actions["getRemoteCommandList"].url = "http://{0}/sony/acessControl".format(self.host) # make sure we are authenticated before self.recreate_authentication() - + if services is not None: # read service list for service in services: - service_id = service.find("{0}serviceId".format(urn_upnp_device)) + service_id = service.find( + "{0}serviceId".format(urn_upnp_device)) if service_id == None: continue if "urn:schemas-sony-com:serviceId:IRCC" not in service_id.text: @@ -219,7 +218,8 @@ def update_service_urls(self): for element in xml_data.findall("supportFunction"): for function in element.findall("function"): if function.attrib["name"] == "WOL": - self.mac = function.find("functionItem").attrib["value"] + self.mac = function.find( + "functionItem").attrib["value"] # get control data for sending commands response = self.send_http(self.dmr_url, method=HttpMethod.GET) @@ -227,7 +227,8 @@ def update_service_urls(self): raw_data = response.text xml_data = xml.etree.ElementTree.fromstring(raw_data) for device in xml_data.findall("{0}device".format(urn_upnp_device)): - serviceList = device.find("{0}serviceList".format(urn_upnp_device)) + serviceList = device.find( + "{0}serviceList".format(urn_upnp_device)) for service in serviceList: service_id = service.find( "{0}serviceId".format(urn_upnp_device)) @@ -243,8 +244,8 @@ def update_service_urls(self): self.update_applist() def update_commands(self): - - # needs to be registred to do that + + # needs to be registred to do that if self.pin is None: return @@ -287,7 +288,6 @@ def update_applist(self, log_errors=True): def recreate_authentication(self): """ The default cookie is for URL/sony. For some commands we need it for the root path. - Only for api v4 """ if self.pin == None: @@ -295,13 +295,13 @@ def recreate_authentication(self): # todo fix cookies cookies = None - #cookies = requests.cookies.RequestsCookieJar() - #cookies.set("auth", self.cookies.get("auth")) - + # cookies = requests.cookies.RequestsCookieJar() + # cookies.set("auth", self.cookies.get("auth")) + username = '' base64string = base64.encodebytes(('%s:%s' % (username, self.pin)).encode()) \ .decode().replace('\n', '') - + registration_action = self.get_action("register") self.headers['Authorization'] = "Basic %s" % base64string @@ -314,19 +314,20 @@ def recreate_authentication(self): def register(self): """ - Register at the api.50001 - :param str name: The name which will be displayed in the UI of the device. Make sure this name does not exist yet + Register at the api. The name which will be displayed in the UI of the device. Make sure this name does not exist yet For this the device must be put in registration mode. The tested sd5500 has no separte mode but allows registration in the overview " """ - registrataion_result = AuthenicationResult.ERROR - registration_action = registration_action = self.get_action("register") + registration_result = AuthenicationResult.ERROR + + registration_action = registration_action = self.get_action("register") + # protocoll version 1 and 2 if registration_action.mode < 3: self.send_http( registration_action.url, method=HttpMethod.GET, raise_errors=True) - registrataion_result = AuthenicationResult.SUCCESS + registration_result = AuthenicationResult.SUCCESS # protocoll version 3 elif registration_action.mode == 3: @@ -335,53 +336,62 @@ def register(self): method=HttpMethod.GET, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registrataion_result = AuthenicationResult.PIN_NEEDED + registration_result = AuthenicationResult.PIN_NEEDED # newest protocoll version 4 this is the same method as braviarc uses elif registration_action.mode == 4: authorization = json.dumps( - { + { + "id": 13, "method": "actRegister", - "params": [{"clientid": self.nickname, - "nickname": self.nickname, - "level": "private"}, - [{"value": "yes", - "function": "WOL"}]], - "id": 1, - "version": "1.0"} - ).encode('utf-8') + "version": "1.0", + "params": [{ + "clientid": self.nickname, + "nickname": self.nickname + }, + [{ + "clientid": self.nickname, + "nickname": self.nickname, + "value": "yes", + "function": "WOL" + }] + ] + }) try: - response = self.send_http(registration_action.url, method=HttpMethod.POST, + headers={ + "Content-Type": "application/json" + } + response=self.send_http(registration_action.url, method=HttpMethod.POST, headers=headers, data=authorization, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registrataion_result = AuthenicationResult.PIN_NEEDED + registration_result=AuthenicationResult.PIN_NEEDED except Exception as ex: # pylint: disable=broad-except _LOGGER.error("[W] Exception: " + str(ex)) else: - resp = response.json() + resp=response.json() _LOGGER.debug(json.dumps(resp, indent=4)) if resp is None or not resp.get('error'): - self.cookies = response.cookies - registrataion_result = AuthenicationResult.SUCCESS + self.cookies=response.cookies + registration_result=AuthenicationResult.SUCCESS else: raise ValueError( "Regisration mode {0} is not supported".format(registration_action.mode)) - return registrataion_result + return registration_result def send_authentication(self, pin): - + registration_action = self.get_action("register") # they do not need a pin if registration_action.mode < 3: return True - self.pin = pin + self.pin=pin self.recreate_authentication() if registration_action.mode == 3: @@ -390,45 +400,21 @@ def send_authentication(self, pin): self.get_action("register").url, method=HttpMethod.GET, raise_errors=True) except: return False - else: - self.pin = pin - return True - return False + return True elif registration_action.mode == 4: - authorization = json.dumps( - { - "id": 13, - "method": "actRegister", - "version": "1.0", - "params": [ - { - "clientid": self.name, - "nickname": self.name, - }, - [ - { - "clientid": self.name, - "value": self.name, - "nickname": self.name, - "function": "WOL" - } - ] - ] - } - ) + url = "http://{0}/sony/appControl".format(self.host) try: - response = self.send_http(self.get_action("register").url, method=HttpMethod.post, - data=authorization, raise_errors=True) + response=self.send_http(url, method=HttpMethod.get, raise_errors=True) except: return False else: - resp = response.json() + resp=response.json() _LOGGER.debug(json.dumps(resp, indent=4)) if resp is None or not resp.get('error'): - self.cookies = response.cookies - self.pin = pin + self.cookies=response.cookies + self.pin=pin return True return False @@ -436,26 +422,27 @@ def send_http(self, url, method, data=None, headers=None, log_errors=True, raise """ Send request command via HTTP json to Sony Bravia.""" if headers is None: - headers = self.headers + headers=self.headers if url is None: return - _LOGGER.debug("Calling http url {0} method {1}".format(url, str(method))) + _LOGGER.debug( + "Calling http url {0} method {1}".format(url, str(method))) try: - params = "" + params="" if data is not None: - params = data.encode("UTF-8") + params=data.encode("UTF-8") if method == HttpMethod.POST: - response = requests.post(url, + response=requests.post(url, data=params, headers=headers, cookies=self.cookies, timeout=TIMEOUT) elif method == HttpMethod.GET: - response = requests.get(url, + response=requests.get(url, data=params, headers=headers, cookies=self.cookies, @@ -476,67 +463,65 @@ def send_http(self, url, method, data=None, headers=None, log_errors=True, raise return response def post_soap_request(self, url, params, action): - headers = { + headers={ 'SOAPACTION': '"{0}"'.format(action), "Content-Type": "text/xml" } - data = "" +\ "" +\ params +\ "" +\ "" - response = self.send_http(url, method=HttpMethod.POST,headers=headers, data=data) + response=self.send_http( + url, method=HttpMethod.POST, headers=headers, data=data) if response is not None: return response.content.decode("utf-8") def send_req_ircc(self, params, log_errors=True): """Send an IRCC command via HTTP to Sony Bravia.""" - data = "" +\ + data="" +\ "" + params + "" +\ "" - action = "urn:schemas-sony-com:service:IRCC:1#X_SendIRCC" + action="urn:schemas-sony-com:service:IRCC:1#X_SendIRCC" - content = self.post_soap_request( + content=self.post_soap_request( url=self.control_url, params=data, action=action) return content def get_playing_status(self): - data = '' + \ + data='' + \ '0' + \ '' - action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" + action="urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" - content = self.post_soap_request( + content=self.post_soap_request( url=self.av_transport_url, params=data, action=action) if None is content: return "OFF" - response = xml.etree.ElementTree.fromstring(content) - state = response.find(".//CurrentTransportState").text + response=xml.etree.ElementTree.fromstring(content) + state=response.find(".//CurrentTransportState").text return state def get_power_status(self): - url = self.actionlist_url + url=self.actionlist_url try: - response = self.send_http(url, HttpMethod.GET, + response=self.send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) except Exception as ex: _LOGGER.debug(ex) return False return True - # def get_source(self, source): - # pass - def start_app(self, app_name, log_errors=True): """Start an app by name""" # sometimes device does not start app if already running one self.home() - url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) - data = "LOCATION: {0}/run".format(url) + url="{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) + data="LOCATION: {0}/run".format(url) self.send_http(url, HttpMethod.POST, data=data) pass @@ -547,9 +532,15 @@ def send_command(self, name): self.send_req_ircc(self.commands[name].value) def get_action(self, name): - if not name in self.actions and len(self.actions) == 0: + if not name in self.actions and len(self.actions) == 0: self.update_service_urls() if not name in self.actions and len(self.actions) == 0: + if name == "register": + registration_action = XmlApiObject(None) + registration_action.url = "http://{0}/sony/accessControl".format( + self.host) + registration_action.mode = 4 + return registration_action raise ValueError('Failed to read action list from device.') return self.actions[name] @@ -560,10 +551,9 @@ def power(self, on): if not self.get_power_status(): self.send_command('Power') else: + # Try using the power on command incase the WOL doesn't work self.send_command('Power') - # Try using the power on command incase the WOL doesn't work - def get_apps(self): return list(self.apps.keys()) @@ -711,3 +701,25 @@ def browserBookmarkList(self): def list(self): self.send_command('List') + + + +if __name__ == "__main__": + + stored_config="bluray.json" + device=None + # device must be on for registration + host="192.168.178.23" + device=SonyDevice(host, "SonyApiLib Python Test") + device.register() + pin=input("Enter the PIN displayed at your device: ") + device.send_authentication(pin) + # save_device() + + + apps=device.get_apps() + + device.start_app(apps[0]) + + # Play media + device.play() From 4f9a6c343f72ff37b2fe9c8e8cc45bdbb94a2df5 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 27 Mar 2019 14:01:42 +0100 Subject: [PATCH 021/170] Revert "Fixed registration url for gen2 devices" This reverts commit dd519f331a1b1c4377f77429dad8b3bac066f376. --- sonyapilib/device.py | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 1fbd648..8ed3569 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -23,7 +23,7 @@ TIMEOUT = 5 -class AuthenticationResult(Enum): +class AuthenicationResult(Enum): SUCCESS = 0 ERROR = 1 PIN_NEEDED = 2 @@ -179,19 +179,9 @@ def update_service_urls(self): # some data has to overwritten for the registration to work properly if action.name == "register": if action.mode < 4: - if action.mode == 1 or action.mode == 3: - action.url = "{0}?name={1}®istrationType=initial&deviceId={2}"\ - .format( - action.url, - urllib.parse.quote(self.nickname), - urllib.parse.quote(self.get_device_id())) - elif action.mode == 2: - action.url = "{0}?name={1}®istrationType=new&deviceId={2}".format( - action.url, - urllib.parse.quote(self.nickname) - urllib.parse.quote(self.get_device_id())) - # the authenication later on is based on the device id and the mac + action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format( + action.url, urllib.parse.quote(self.nickname)) if action.mode == 3: action.url = action.url + "&wolSupport=true" elif action.mode == 4: @@ -326,26 +316,30 @@ def register(self): """ Register at the api. The name which will be displayed in the UI of the device. Make sure this name does not exist yet For this the device must be put in registration mode. + The tested sd5500 has no separte mode but allows registration in the overview " """ - registration_result = AuthenticationResult.ERROR + registrataion_result = AuthenicationResult.ERROR registration_action = registration_action = self.get_action("register") - # protocol version 1 and 2 + # protocoll version 1 and 2 if registration_action.mode < 3: self.send_http( registration_action.url, method=HttpMethod.GET, raise_errors=True) - registration_result = AuthenticationResult.SUCCESS + if registration_response.text == "": + registration_response = AuthenicationResult.SUCCESS + else: + registration_response = AuthenicationResult.ERROR - # protocol version 3 + # protocoll version 3 elif registration_action.mode == 3: try: self.send_http(registration_action.url, method=HttpMethod.GET, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registration_result = AuthenticationResult.PIN_NEEDED + registrataion_result = AuthenicationResult.PIN_NEEDED - # newest protocol version 4 this is the same method as braviarc uses + # newest protocoll version 4 this is the same method as braviarc uses elif registration_action.mode == 4: authorization = json.dumps( { @@ -364,7 +358,7 @@ def register(self): data=authorization, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registration_result = AuthenticationResult.PIN_NEEDED + registrataion_result = AuthenicationResult.PIN_NEEDED except Exception as ex: # pylint: disable=broad-except _LOGGER.error("[W] Exception: " + str(ex)) @@ -373,13 +367,13 @@ def register(self): _LOGGER.debug(json.dumps(resp, indent=4)) if resp is None or not resp.get('error'): self.cookies = response.cookies - registration_result = AuthenticationResult.SUCCESS + registrataion_result = AuthenicationResult.SUCCESS else: raise ValueError( "Regisration mode {0} is not supported".format(registration_action.mode)) - return registration_result + return registrataion_result def send_authentication(self, pin): From d08fe8e2448b9f96efb403ff29bda04bac0ac546 Mon Sep 17 00:00:00 2001 From: dilruacs Date: Thu, 28 Mar 2019 08:56:44 +0100 Subject: [PATCH 022/170] Update README.md --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index aad28e0..3e234e6 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,11 @@ if __name__ == "__main__": device.play() ``` +# URL list + +https://github.com/chr15m/media-remote/blob/master/SNIFF.md +https://gist.github.com/kalleth/e10e8f3b8b7cb1bac21463b0073a65fb + # Compatability List LCD TV BRAVIA From fadc5bb56837dd99849824fc4af722822053bd3e Mon Sep 17 00:00:00 2001 From: dilruacs Date: Thu, 28 Mar 2019 08:57:16 +0100 Subject: [PATCH 023/170] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3e234e6..328bee4 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ if __name__ == "__main__": # URL list https://github.com/chr15m/media-remote/blob/master/SNIFF.md + https://gist.github.com/kalleth/e10e8f3b8b7cb1bac21463b0073a65fb # Compatability List From a1727571f67a60e81020ea882a8cb1c32839ddcc Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Fri, 29 Mar 2019 22:27:47 +0100 Subject: [PATCH 024/170] Spelling error and variable mixup? --- sonyapilib/device.py | 56 +++++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index e3aa84e..aede060 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -20,7 +20,7 @@ from sonyapilib import ssdp _LOGGER = logging.getLogger(__name__) -TIMEOUT = 50 +TIMEOUT = 5 class AuthenicationResult(Enum): @@ -54,7 +54,7 @@ def __init__(self, xml_data): if (arg == "mode"): setattr(self, arg, int(xml_data[arg])) else: - setattr(self, arg, xml_data[arg]) + setattr(self, arg, xml_data[arg]) class SonyDevice(): """ @@ -95,7 +95,7 @@ def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, i if len(self.actions) == 0 and self.pin is not None: self.update_service_urls() - + @staticmethod def discover(): """ @@ -115,7 +115,7 @@ def load_from_json(data): return jsonpickle.decode(data) def save_to_json(self): - + return jsonpickle.dumps(self) def create_json_v4(self, method, params=None): @@ -175,7 +175,7 @@ def update_service_urls(self): for element in xml_data.findall("action"): action = XmlApiObject(element.attrib) self.actions[action.name] = action - + # some data has to overwritten for the registration to work properly if action.name == "register": if action.mode < 4: @@ -191,10 +191,10 @@ def update_service_urls(self): # if self.actions["register"].mode == 4: # self.actions["getRemoteCommandList"].url = "http://{0}/sony/system".format( # lirc_url.netloc.split(":")[0]) - + # make sure we are authenticated before self.recreate_authentication() - + if services is not None: # read service list for service in services: @@ -243,8 +243,8 @@ def update_service_urls(self): self.update_applist() def update_commands(self): - - # needs to be registred to do that + + # needs to be registred to do that if self.pin is None: return @@ -297,16 +297,16 @@ def recreate_authentication(self): cookies = None #cookies = requests.cookies.RequestsCookieJar() #cookies.set("auth", self.cookies.get("auth")) - + username = '' base64string = base64.encodebytes(('%s:%s' % (username, self.pin)).encode()) \ .decode().replace('\n', '') - + registration_action = self.get_action("register") self.headers['Authorization'] = "Basic %s" % base64string if registration_action.mode == 3: - self.headers['X-CERS-DEVICE-ID'] = self.nickname + self.headers['X-CERS-DEVICE-ID'] = self.get_device_id() elif registration_action.mode == 4: self.headers['Connection'] = "keep-alive" @@ -315,21 +315,21 @@ def recreate_authentication(self): def register(self): """ Register at the api.50001 - :param str name: The name which will be displayed in the UI of the device. Make sure this name does not exist yet + Register at the api The name which will be displayed in the UI of the device. Make sure this name does not exist yet For this the device must be put in registration mode. The tested sd5500 has no separte mode but allows registration in the overview " """ - registrataion_result = AuthenicationResult.ERROR + registration_result = AuthenicationResult.ERROR registration_action = registration_action = self.get_action("register") # protocoll version 1 and 2 if registration_action.mode < 3: - registration_response = self.send_http( + self.send_http( registration_action.url, method=HttpMethod.GET, raise_errors=True) if registration_response.text == "": - registrataion_result = AuthenicationResult.SUCCESS + registration_result = AuthenicationResult.SUCCESS else: - registrataion_result = AuthenicationResult.ERROR + registration_result = AuthenicationResult.ERROR # protocoll version 3 elif registration_action.mode == 3: @@ -338,7 +338,7 @@ def register(self): method=HttpMethod.GET, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registrataion_result = AuthenicationResult.PIN_NEEDED + registration_result = AuthenicationResult.PIN_NEEDED # newest protocoll version 4 this is the same method as braviarc uses elif registration_action.mode == 4: @@ -359,7 +359,7 @@ def register(self): data=authorization, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registrataion_result = AuthenicationResult.PIN_NEEDED + registration_result = AuthenicationResult.PIN_NEEDED except Exception as ex: # pylint: disable=broad-except _LOGGER.error("[W] Exception: " + str(ex)) @@ -368,17 +368,22 @@ def register(self): _LOGGER.debug(json.dumps(resp, indent=4)) if resp is None or not resp.get('error'): self.cookies = response.cookies - registrataion_result = AuthenicationResult.SUCCESS + registration_result = AuthenicationResult.SUCCESS else: raise ValueError( "Regisration mode {0} is not supported".format(registration_action.mode)) - return registrataion_result + return registration_result def send_authentication(self, pin): + registration_action = self.get_action("register") + # they do not need a pin + if registration_action.mode < 3: + return True + self.pin = pin self.recreate_authentication() @@ -501,6 +506,9 @@ def send_req_ircc(self, params, log_errors=True): url=self.control_url, params=data, action=action) return content + def get_device_id(self): + return "TVSideView:{0}".format(self.mac) + def get_playing_status(self): data = '' + \ '0' + \ @@ -519,7 +527,7 @@ def get_playing_status(self): def get_power_status(self): url = self.actionlist_url try: - responst = self.send_http(url, HttpMethod.GET, + response = self.send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) except Exception as ex: _LOGGER.debug(ex) @@ -545,7 +553,7 @@ def send_command(self, name): self.send_req_ircc(self.commands[name].value) def get_action(self, name): - if not name in self.actions and len(self.actions) == 0: + if not name in self.actions and len(self.actions) == 0: self.update_service_urls() if not name in self.actions and len(self.actions) == 0: raise ValueError('Failed to read action list from device.') @@ -561,7 +569,7 @@ def power(self, on): self.send_command('Power') # Try using the power on command incase the WOL doesn't work - + def get_apps(self): return list(self.apps.keys()) From ab2ff9a69c93aee60b8f9301cccc77c092590853 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Fri, 29 Mar 2019 22:35:29 +0100 Subject: [PATCH 025/170] Added "registration_response = " --- sonyapilib/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index aede060..e346202 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -324,7 +324,7 @@ def register(self): # protocoll version 1 and 2 if registration_action.mode < 3: - self.send_http( + registration_response = self.send_http( registration_action.url, method=HttpMethod.GET, raise_errors=True) if registration_response.text == "": registration_result = AuthenicationResult.SUCCESS From a5b087fb9ccd0ba5e18f13198b291cc9af0bf060 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 29 Mar 2019 23:14:21 +0100 Subject: [PATCH 026/170] Added working registration --- sonyapilib/device.py | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index e11ecb7..e03befa 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -403,20 +403,38 @@ def send_authentication(self, pin): return True elif registration_action.mode == 4: - url = "http://{0}/sony/appControl".format(self.host) + + # todo refactor to remove duplicated code from register + authorization = json.dumps( + { + "id": 13, + "method": "actRegister", + "version": "1.0", + "params": [{ + "clientid": self.nickname, + "nickname": self.nickname + }, + [{ + "clientid": self.nickname, + "nickname": self.nickname, + "value": "yes", + "function": "WOL" + }] + ] + }) try: - response=self.send_http(url, method=HttpMethod.get, raise_errors=True) - except: + response=self.send_http(registration_action.url, method=HttpMethod.POST, + data=authorization, raise_errors=True) + self.cookies = response.cookies + self.cookies.set(name="auth", value=response.cookies['auth'], path="/") + + # todo implement request json + foobar = self.send_http("http://192.168.178.23/DIAL/sony/applist", method=HttpMethod.GET,data=authorization, raise_errors=True) + _LOGGER.error(json.dumps(foobar)) + except requests.exceptions.HTTPError as ex: return False - else: - resp=response.json() - _LOGGER.debug(json.dumps(resp, indent=4)) - if resp is None or not resp.get('error'): - self.cookies=response.cookies - self.pin=pin - return True - return False + return True def send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): """ Send request command via HTTP json to Sony Bravia.""" @@ -710,7 +728,7 @@ def list(self): device=None # device must be on for registration host="192.168.178.23" - device=SonyDevice(host, "SonyApiLib Python Test") + device=SonyDevice(host, "SonyApiLib Python Test10") device.register() pin=input("Enter the PIN displayed at your device: ") device.send_authentication(pin) From 1d2d3158ecf27431047467209b11955205c6670b Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 30 Mar 2019 00:22:16 +0100 Subject: [PATCH 027/170] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index aad28e0..7345387 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Sonyapilib Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. -It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant - +It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant wich you can find here: +https://github.com/dilruacs/home-assistant-custom-components/tree/master/custom_components/sony # Installation ``` pip install sonyapilib From 2106105ebfeaafb7f6fcaa73e104b14b3020905d Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Mon, 1 Apr 2019 08:08:37 +0200 Subject: [PATCH 028/170] imported logging twice --- sonyapilib/device.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index dfd6f96..71b1dd2 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -10,7 +10,6 @@ import requests import urllib.parse import xml.etree.ElementTree -import logging import requests from enum import Enum @@ -377,7 +376,7 @@ def register(self): return registration_result def send_authentication(self, pin): - + registration_action = self.get_action("register") # they do not need a pin From d366961316260a3643d1fc8f0b161f49055ed9a9 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Mon, 1 Apr 2019 08:08:54 +0200 Subject: [PATCH 029/170] Spelling, version --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 65d2171..639caf3 100644 --- a/setup.py +++ b/setup.py @@ -16,11 +16,11 @@ setup(name='sonyapilib', packages = ['sonyapilib'], # this must be the same as the name above version = '0.3.10', - description = 'Lib to control sony devices with theier soap api', + description = 'Lib to control sony devices with their soap api', author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', url = 'https://github.com/alexmohr/sonyapilib', # use the URL to the github repo - download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3.7', + download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3.10', keywords = ['soap', 'sony', 'api'], # arbitrary keywords classifiers = [], install_requires=[ @@ -29,4 +29,4 @@ 'requests' ], -) \ No newline at end of file +) From 9c9d6c0d71ced325b7fb456a89e75a9ee257c4b1 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Mon, 1 Apr 2019 08:25:39 +0200 Subject: [PATCH 030/170] Use wakeonlan library --- sonyapilib/device.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 71b1dd2..c63c8d1 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -13,7 +13,7 @@ import requests from enum import Enum - +import wakeonlan import jsonpickle from sonyapilib import ssdp @@ -129,19 +129,7 @@ def create_json_v4(self, method, params=None): def wakeonlan(self): if self.mac is not None: - addr_byte = self.mac.split('-') - hw_addr = struct.pack('BBBBBB', int(addr_byte[0], 16), - int(addr_byte[1], 16), - int(addr_byte[2], 16), - int(addr_byte[3], 16), - int(addr_byte[4], 16), - int(addr_byte[5], 16)) - msg = b'\xff' * 6 + hw_addr * 16 - socket_instance = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) - socket_instance.setsockopt( - socket.SOL_SOCKET, socket.SO_BROADCAST, 1) - socket_instance.sendto(msg, ('', 9)) - socket_instance.close() + wakeonlan.send_magic_packet(self.mac, self.host) def update_service_urls(self): """ Initalizes the device by reading the necessary resources from it """ From c520ce64d9438bc81c3d8b75319cd825625e9e75 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Mon, 1 Apr 2019 11:10:39 +0200 Subject: [PATCH 031/170] modify mac --- sonyapilib/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index c63c8d1..2b4cc96 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -129,7 +129,7 @@ def create_json_v4(self, method, params=None): def wakeonlan(self): if self.mac is not None: - wakeonlan.send_magic_packet(self.mac, self.host) + wakeonlan.send_magic_packet(self.mac.replace('-',':'), self.host) def update_service_urls(self): """ Initalizes the device by reading the necessary resources from it """ From 19066fc23c4f457bb4950081b2b44fcb471f0ed3 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Mon, 1 Apr 2019 11:13:48 +0200 Subject: [PATCH 032/170] Add debug --- sonyapilib/device.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 2b4cc96..6193e1c 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -129,6 +129,7 @@ def create_json_v4(self, method, params=None): def wakeonlan(self): if self.mac is not None: + _LOGGER.debug("Waking up %s at %s", self.mac, self.host) wakeonlan.send_magic_packet(self.mac.replace('-',':'), self.host) def update_service_urls(self): From 229c74ca12b7c50af05dfab4df44e752fc1f6ff1 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Mon, 1 Apr 2019 11:20:07 +0200 Subject: [PATCH 033/170] add parameter name --- sonyapilib/device.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 6193e1c..eb1843d 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -129,8 +129,7 @@ def create_json_v4(self, method, params=None): def wakeonlan(self): if self.mac is not None: - _LOGGER.debug("Waking up %s at %s", self.mac, self.host) - wakeonlan.send_magic_packet(self.mac.replace('-',':'), self.host) + wakeonlan.send_magic_packet(self.mac, ip_address=self.host) def update_service_urls(self): """ Initalizes the device by reading the necessary resources from it """ From 2bc2c84676cb1c576fb29ed56c41b067f39c2c34 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Mon, 1 Apr 2019 12:59:34 +0200 Subject: [PATCH 034/170] Add error checking to send_command --- sonyapilib/device.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index eb1843d..b715442 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -537,7 +537,10 @@ def send_command(self, name): if len(self.commands) == 0: self.update_commands() - self.send_req_ircc(self.commands[name].value) + if len(self.commands) > 0: + self.send_req_ircc(self.commands[name].value) + else: + raise ValueError('Failed to read command list from device.') def get_action(self, name): if not name in self.actions and len(self.actions) == 0: From 1d240b8b6bef20a7405c9cf2144004bf9e3a873d Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Mon, 1 Apr 2019 13:01:27 +0200 Subject: [PATCH 035/170] Check if command is known --- sonyapilib/device.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index b715442..8b37211 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -538,7 +538,10 @@ def send_command(self, name): self.update_commands() if len(self.commands) > 0: - self.send_req_ircc(self.commands[name].value) + if name in self.commands: + self.send_req_ircc(self.commands[name].value) + else: + raise ValueError('Unknown command: %s', name) else: raise ValueError('Failed to read command list from device.') From 690aef90f70e224b7ebc04e0387e56cc221dbb1d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 1 Apr 2019 17:18:04 +0200 Subject: [PATCH 036/170] Fixed spelling --- sonyapilib/device.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 8b37211..5ad2875 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -22,7 +22,7 @@ TIMEOUT = 5 -class AuthenicationResult(Enum): +class AuthenticationResult(Enum): SUCCESS = 0 ERROR = 1 PIN_NEEDED = 2 @@ -306,7 +306,7 @@ def register(self): For this the device must be put in registration mode. The tested sd5500 has no separte mode but allows registration in the overview " """ - registration_result = AuthenicationResult.ERROR + registration_result = AuthenticationResult.ERROR registration_action = registration_action = self.get_action("register") # protocoll version 1 and 2 @@ -314,9 +314,9 @@ def register(self): registration_response = self.send_http( registration_action.url, method=HttpMethod.GET, raise_errors=True) if registration_response.text == "": - registration_result = AuthenicationResult.SUCCESS + registration_result = AuthenticationResult.SUCCESS else: - registration_result = AuthenicationResult.ERROR + registration_result = AuthenticationResult.ERROR # protocoll version 3 elif registration_action.mode == 3: @@ -325,7 +325,7 @@ def register(self): method=HttpMethod.GET, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registration_result = AuthenicationResult.PIN_NEEDED + registration_result = AuthenticationResult.PIN_NEEDED # newest protocoll version 4 this is the same method as braviarc uses elif registration_action.mode == 4: @@ -346,7 +346,7 @@ def register(self): data=authorization, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registration_result = AuthenicationResult.PIN_NEEDED + registration_result = AuthenticationResult.PIN_NEEDED except Exception as ex: # pylint: disable=broad-except _LOGGER.error("[W] Exception: " + str(ex)) @@ -355,7 +355,7 @@ def register(self): _LOGGER.debug(json.dumps(resp, indent=4)) if resp is None or not resp.get('error'): self.cookies = response.cookies - registration_result = AuthenicationResult.SUCCESS + registration_result = AuthenticationResult.SUCCESS else: raise ValueError( From 9169bae8b3ef1253423320246546b17a02f6ce2d Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 1 Apr 2019 17:24:10 +0200 Subject: [PATCH 037/170] Moved examples to subfolder and updated readme. --- README.md | 4 +- .../convert_to_hass.py | 0 .../discover_devices.py | 0 examples/pair_and_apps.py | 37 +++++++++++++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) rename convert_to_hass.py => examples/convert_to_hass.py (100%) rename discover_devices.py => examples/discover_devices.py (100%) create mode 100644 examples/pair_and_apps.py diff --git a/README.md b/README.md index 890968a..43e1118 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,15 @@ if __name__ == "__main__": device.play() ``` +More examples can be found in the examples folder. + # URL list https://github.com/chr15m/media-remote/blob/master/SNIFF.md https://gist.github.com/kalleth/e10e8f3b8b7cb1bac21463b0073a65fb -# Compatability List +# Compatibility List LCD TV BRAVIA 2016 model or later: diff --git a/convert_to_hass.py b/examples/convert_to_hass.py similarity index 100% rename from convert_to_hass.py rename to examples/convert_to_hass.py diff --git a/discover_devices.py b/examples/discover_devices.py similarity index 100% rename from discover_devices.py rename to examples/discover_devices.py diff --git a/examples/pair_and_apps.py b/examples/pair_and_apps.py new file mode 100644 index 0000000..35bd635 --- /dev/null +++ b/examples/pair_and_apps.py @@ -0,0 +1,37 @@ +from sonyapilib.device import SonyDevice + +def save_device(): + data = device.save_to_json() + text_file = open("bluray.json", "w") + text_file.write(data) + text_file.close() + +if __name__ == "__main__": + + stored_config = "bluray.json" + device = None + import os.path + if os.path.exists(stored_config): + with open(stored_config, 'r') as content_file: + json_data = content_file.read() + device = SonyDevice.load_from_json(json_data) + else: + # device must be on for registration + host = "10.0.0.102" + device = SonyDevice(host, "SonyApiLib Python Test") + device.register() + pin = input("Enter the PIN displayed at your device: ") + device.send_authentication(pin) + save_device() + + # wake device + is_on = device.get_power_status() + if not is_on: + device.power(True) + + apps = device.get_apps() + + device.start_app(apps[0]) + + # Play media + device.play() From 7308575845279f21917c179cb416960f7174e309 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 1 Apr 2019 17:26:15 +0200 Subject: [PATCH 038/170] Added wakeonlan library to pip dependencies. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 639caf3..01ad8d3 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,8 @@ install_requires=[ 'jsonpickle', 'setuptools', - 'requests' + 'requests', + 'wakeonlan' ], ) From 04b164455b03904ef39225e0f2b70724688f8bbf Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 1 Apr 2019 21:46:00 +0200 Subject: [PATCH 039/170] refactoring in parts to include v4 --- sonyapilib/device.py | 364 ++++++++++++++++++++----------------------- 1 file changed, 173 insertions(+), 191 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 15bc410..87e3930 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -20,6 +20,9 @@ _LOGGER = logging.getLogger(__name__) TIMEOUT = 5 +URN_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}" +URN_SONY_AV = "{urn:schemas-sony-com:av}" + class AuthenticationResult(Enum): @@ -55,22 +58,24 @@ def __init__(self, xml_data): else: setattr(self, arg, xml_data[arg]) + class SonyDevice(): """ Contains all data for the device """ - def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, ircc_location=None): + def __init__(self, host, nickname): """ Init the device with the entry point""" self.host = host self.nickname = nickname - self.ircc_url = ircc_location self.actionlist_url = None self.control_url = None self.av_transport_url = None self.app_url = None - self.app_port = app_port + self.app_port = 50202 + self.dmr_port = 52323 + self.ircc_port = 50001 self.actions = {} self.headers = {} @@ -81,25 +86,18 @@ def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, i self.name = None self.cookies = None self.mac = None - self.authenticated = False - - if self.ircc_url == None: - self.ircc_url = "http://{0}:{1}/Ircc.xml".format(host, port) - - self.dmr_port = dmr_port + self.is_v4 = False - self.dmr_url = "http://{0}:{1}/dmr.xml".format( - self.host, self.dmr_port) + self.ircc_url = "http://{0}:{1}/Ircc.xml".format(host, self.ircc_port) + self.irccscpd_url = "http://{0}:{1}/IRCCSCPD.xml".format(host, self.ircc_port) + self.dmr_url = "http://{0}:{1}/dmr.xml".format(self.host, self.dmr_port) self.app_url = "http://{0}:{1}".format(self.host, self.app_port) - if len(self.actions) == 0 and self.pin is not None: - self.update_service_urls() - @staticmethod def discover(): - """ - Discover all available devices. - """ + """ Discover all available devices. """ + + # Todo check if this works with v4 discovery = ssdp.SSDPDiscovery() devices = [] for device in discovery.discover("urn:schemas-sony-com:service:headersIRCC:1"): @@ -114,48 +112,52 @@ def load_from_json(data): return jsonpickle.decode(data) def save_to_json(self): - + """ Save this device configuration into a json """ return jsonpickle.dumps(self) - def create_json_v4(self, method, params=None): - """ Create json data which will be send via post for the V4 api""" - if params is not None: - ret = json.dumps({"method": method, "params": [ - params], "id": 1, "version": "1.0"}) - else: - ret = json.dumps({"method": method, "params": [], - "id": 1, "version": "1.0"}) - return ret - def wakeonlan(self): if self.mac is not None: wakeonlan.send_magic_packet(self.mac, ip_address=self.host) - def update_service_urls(self): - """ Initalizes the device by reading the necessary resources from it """ + def _update_service_urls(self): + """ Initialize the device by reading the necessary resources from it """ + self._parse_dmr() + + self.recreate_authentication() + if self.is_v4: + self._parse_irccscpd() + else: + self._parse_ircc() + + if len(self.commands) > 0: + self.update_commands() + self.update_applist() - lirc_url = urllib.parse.urlparse(self.ircc_url) - response = self.send_http(self.ircc_url, method=HttpMethod.GET) + def _parse_irccscpd(self): + response = self._send_http(self.irccscpd_url, method=HttpMethod.GET) if response is None: return - raw_data = response.text - xml_data = xml.etree.ElementTree.fromstring(raw_data) - urn_upnp_device = "{urn:schemas-upnp-org:device-1-0}" - urn_sony_av = "{urn:schemas-sony-com:av}" + def _parse_ircc(self): + response = self._send_http(self.ircc_url, method=HttpMethod.GET) + if response is None: + return + raw_data = response.text + xml_data = xml.etree.ElementTree.fromstring(raw_data) + # the action list contains everything the device supports - self.actionlist_url = xml_data.find("{0}device".format(urn_upnp_device))\ - .find("{0}X_UNR_DeviceInfo".format(urn_sony_av))\ - .find("{0}X_CERS_ActionList_URL".format(urn_sony_av))\ + self.actionlist_url = xml_data.find("{0}device".format(URN_UPNP_DEVICE))\ + .find("{0}X_UNR_DeviceInfo".format(URN_SONY_AV))\ + .find("{0}X_CERS_ActionList_URL".format(URN_SONY_AV))\ .text - services = xml_data.find("{0}device".format(urn_upnp_device))\ - .find("{0}serviceList".format(urn_upnp_device))\ - .findall("{0}service".format(urn_upnp_device)) + services = xml_data.find("{0}device".format(URN_UPNP_DEVICE))\ + .find("{0}serviceList".format(URN_UPNP_DEVICE))\ + .findall("{0}service".format(URN_UPNP_DEVICE)) # read action list - response = self.send_http(self.actionlist_url, method=HttpMethod.GET) + response = self._send_http(self.actionlist_url, method=HttpMethod.GET) if response is not None: raw_data = response.text xml_data = xml.etree.ElementTree.fromstring(raw_data) @@ -168,34 +170,28 @@ def update_service_urls(self): if action.mode < 4: # the authentication later on is based on the device id and the mac action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format( - action.url, urllib.parse.quote(self.nickname)) + action.url, urllib.parse.quote(self.nickname)) if action.mode == 3: action.url = action.url + "&wolSupport=true" - elif action.mode == 4: - # overwrite urls for api version 4 to be consistent later. - if self.actions["register"].mode == 4: - self.actions["getRemoteCommandList"].url = "http://{0}/sony/acessControl".format(self.host) - # make sure we are authenticated before - self.recreate_authentication() - + lirc_url = urllib.parse.urlparse(self.ircc_url) if services is not None: # read service list for service in services: service_id = service.find( - "{0}serviceId".format(urn_upnp_device)) + "{0}serviceId".format(URN_UPNP_DEVICE)) if service_id == None: continue if "urn:schemas-sony-com:serviceId:IRCC" not in service_id.text: continue service_location = service.find( - "{0}controlURL".format(urn_upnp_device)).text + "{0}controlURL".format(URN_UPNP_DEVICE)).text service_url = lirc_url.scheme + "://" + lirc_url.netloc self.control_url = service_url + service_location # get systeminformation - response = self.send_http( + response = self._send_http( self.get_action("getSystemInformation").url, method=HttpMethod.GET) if response is not None: @@ -207,37 +203,54 @@ def update_service_urls(self): self.mac = function.find( "functionItem").attrib["value"] + def _parse_dmr(self): # get control data for sending commands - response = self.send_http(self.dmr_url, method=HttpMethod.GET) - if response is not None: - raw_data = response.text - xml_data = xml.etree.ElementTree.fromstring(raw_data) - for device in xml_data.findall("{0}device".format(urn_upnp_device)): - serviceList = device.find( - "{0}serviceList".format(urn_upnp_device)) - for service in serviceList: - service_id = service.find( - "{0}serviceId".format(urn_upnp_device)) - if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: - continue - transport_location = service.find( - "{0}controlURL".format(urn_upnp_device)).text - self.av_transport_url = "{0}://{1}:{2}{3}".format( - lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + response = self._send_http(self.dmr_url, method=HttpMethod.GET) + if response is None: + return + + lirc_url = urllib.parse.urlparse(self.ircc_url) + xml_data = xml.etree.ElementTree.fromstring(response.text) + for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): + serviceList = device.find( + "{0}serviceList".format(URN_UPNP_DEVICE)) + for service in serviceList: + service_id = service.find( + "{0}serviceId".format(URN_UPNP_DEVICE)) + if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: + continue + transport_location = service.find( + "{0}controlURL".format(URN_UPNP_DEVICE)).text + self.av_transport_url = "{0}://{1}:{2}{3}".format( + lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + + # this is only for v4 devices. + if not "av:X_ScalarWebAPI_ServiceType" in response.text: + return + + self.is_v4 = True + for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): + serviceList = device.find("{0}serviceList".format(URN_UPNP_DEVICE)) + for service in serviceList: + service_id = service.find( + "{0}serviceId".format(URN_UPNP_DEVICE)) + if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: + continue + transport_location = service.find( + "{0}controlURL".format(URN_UPNP_DEVICE)).text + self.av_transport_url = "{0}://{1}:{2}{3}".format( + lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) - if len(self.commands) > 0: - self.update_commands() - self.update_applist() def update_commands(self): - # needs to be registred to do that + # needs to be registered to do that if self.pin is None: return url = self.get_action("getRemoteCommandList").url if self.get_action("register").mode < 4: - response = self.send_http(url, method=HttpMethod.GET) + response = self._send_http(url, method=HttpMethod.GET) if response is not None: xml_data = xml.etree.ElementTree.fromstring(response.text) @@ -245,8 +258,8 @@ def update_commands(self): name = command.get("name") self.commands[name] = XmlApiObject(command.attrib) else: - response = self.send_http( - url, method=HttpMethod.POST, data=self.create_json_v4("getRemoteControllerInfo")) + response = self._send_http( + url, method=HttpMethod.POST, data=self._create_api_json("getRemoteControllerInfo", 1)) if response is not None: json = response.json() if not json.get('error'): @@ -255,11 +268,11 @@ def update_commands(self): self.commands = json.get('result')[1] else: _LOGGER.error("JSON request error: " + - json.dumps(json, indent=4)) + json.dumps(json, indent=4)) def update_applist(self, log_errors=True): url = self.app_url + "/appslist" - response = self.send_http(url, method=HttpMethod.GET) + response = self._send_http(url, method=HttpMethod.GET) if response is not None: xml_data = xml.etree.ElementTree.fromstring(response.text) apps = xml_data.findall(".//app") @@ -306,62 +319,47 @@ def register(self): """ registration_result = AuthenticationResult.ERROR - registration_action = registration_action = self.get_action("register") - - # protocoll version 1 and 2 + + # protocol version 1 and 2 if registration_action.mode < 3: - registration_response = self.send_http( + registration_response = self._send_http( registration_action.url, method=HttpMethod.GET, raise_errors=True) registration_result = AuthenticationResult.SUCCESS - # protocoll version 3 + # protocol version 3 elif registration_action.mode == 3: try: - self.send_http(registration_action.url, - method=HttpMethod.GET, raise_errors=True) + self._send_http(registration_action.url, + method=HttpMethod.GET, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) + # todo set the correct result. registration_result = AuthenticationResult.PIN_NEEDED - # newest protocoll version 4 this is the same method as braviarc uses + # newest protocol version 4 this is the same method as braviarc uses elif registration_action.mode == 4: - authorization = json.dumps( - { - "id": 13, - "method": "actRegister", - "version": "1.0", - "params": [{ - "clientid": self.nickname, - "nickname": self.nickname - }, - [{ - "clientid": self.nickname, - "nickname": self.nickname, - "value": "yes", - "function": "WOL" - }] - ] - }) + authorization = self._create_api_json("actRegister", 13) try: - headers={ + headers = { "Content-Type": "application/json" } - response=self.send_http(registration_action.url, method=HttpMethod.POST, headers=headers, - data=authorization, raise_errors=True) + response = self._send_http(registration_action.url, method=HttpMethod.POST, headers=headers, + data=authorization, raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) - registration_result=AuthenticationResult.PIN_NEEDED + # todo set the correct result. + registration_result = AuthenticationResult.PIN_NEEDED except Exception as ex: # pylint: disable=broad-except _LOGGER.error("[W] Exception: " + str(ex)) else: - resp=response.json() + resp = response.json() _LOGGER.debug(json.dumps(resp, indent=4)) if resp is None or not resp.get('error'): - self.cookies=response.cookies - registration_result=AuthenticationResult.SUCCESS + self.cookies = response.cookies + registration_result = AuthenticationResult.SUCCESS else: raise ValueError( @@ -376,56 +374,48 @@ def send_authentication(self, pin): if registration_action.mode < 3: return True - self.pin=pin + self.pin = pin self.recreate_authentication() + self.register() - if registration_action.mode == 3: - try: - self.send_http( - self.get_action("register").url, method=HttpMethod.GET, raise_errors=True) - except: - return False - return True + def _request_json(self, url, params, log_errors=True): + """ Send request command via HTTP json to Sony Bravia.""" + built_url = 'http://{}/{}'.format(self.host, url) - elif registration_action.mode == 4: - - # todo refactor to remove duplicated code from register - authorization = json.dumps( - { - "id": 13, - "method": "actRegister", - "version": "1.0", - "params": [{ - "clientid": self.nickname, - "nickname": self.nickname - }, - [{ - "clientid": self.nickname, - "nickname": self.nickname, - "value": "yes", - "function": "WOL" - }] - ] - }) + response = self._send_http(url, HttpMethod.POST, params) + html = json.loads(response.content.decode('utf-8')) + return html - try: - response=self.send_http(registration_action.url, method=HttpMethod.POST, - data=authorization, raise_errors=True) - self.cookies = response.cookies - self.cookies.set(name="auth", value=response.cookies['auth'], path="/") - - # todo implement request json - foobar = self.send_http("http://192.168.178.23/DIAL/sony/applist", method=HttpMethod.GET,data=authorization, raise_errors=True) - _LOGGER.error(json.dumps(foobar)) - except requests.exceptions.HTTPError as ex: - return False - return True + def _create_api_json(self, method, id, params=None): + """ Create json data which will be send via post for the V4 api""" + if params is None: + params = [{ + "clientid": self.get_device_id(), + "nickname": self.nickname + }, + [{ + "clientid": self.get_device_id(), + "nickname": self.nickname, + "value": "yes", + "function": "WOL" + }] + ] + + ret = json.dumps( + { + "method": method, + "params": params, + "id": id, + "version": "1.0" + }) - def send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): + return ret + + def _send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): """ Send request command via HTTP json to Sony Bravia.""" if headers is None: - headers=self.headers + headers = self.headers if url is None: return @@ -434,18 +424,18 @@ def send_http(self, url, method, data=None, headers=None, log_errors=True, raise "Calling http url {0} method {1}".format(url, str(method))) try: - params="" + params = "" if data is not None: - params=data.encode("UTF-8") + params = data.encode("UTF-8") if method == HttpMethod.POST: - response=requests.post(url, + response = requests.post(url, data=params, headers=headers, cookies=self.cookies, timeout=TIMEOUT) elif method == HttpMethod.GET: - response=requests.get(url, + response = requests.get(url, data=params, headers=headers, cookies=self.cookies, @@ -466,18 +456,18 @@ def send_http(self, url, method, data=None, headers=None, log_errors=True, raise return response def post_soap_request(self, url, params, action): - headers={ + headers = { 'SOAPACTION': '"{0}"'.format(action), "Content-Type": "text/xml" } - data="" +\ "" +\ params +\ "" +\ "" - response=self.send_http( + response = self._send_http( url, method=HttpMethod.POST, headers=headers, data=data) if response is not None: return response.content.decode("utf-8") @@ -485,12 +475,12 @@ def post_soap_request(self, url, params, action): def send_req_ircc(self, params, log_errors=True): """Send an IRCC command via HTTP to Sony Bravia.""" - data="" +\ + data = "" +\ "" + params + "" +\ "" - action="urn:schemas-sony-com:service:IRCC:1#X_SendIRCC" + action = "urn:schemas-sony-com:service:IRCC:1#X_SendIRCC" - content=self.post_soap_request( + content = self.post_soap_request( url=self.control_url, params=data, action=action) return content @@ -498,25 +488,25 @@ def get_device_id(self): return "TVSideView:{0}".format(self.mac) def get_playing_status(self): - data='' + \ - '0' + \ - '' + data = '' + \ + '0' + \ + '' - action="urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" + action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" - content=self.post_soap_request( + content = self.post_soap_request( url=self.av_transport_url, params=data, action=action) if None is content: return "OFF" - response=xml.etree.ElementTree.fromstring(content) - state=response.find(".//CurrentTransportState").text + response = xml.etree.ElementTree.fromstring(content) + state = response.find(".//CurrentTransportState").text return state def get_power_status(self): - url=self.actionlist_url + url = self.actionlist_url try: - response=self.send_http(url, HttpMethod.GET, - log_errors=False, raise_errors=True) + response = self._send_http(url, HttpMethod.GET, + log_errors=False, raise_errors=True) except Exception as ex: _LOGGER.debug(ex) return False @@ -526,9 +516,9 @@ def start_app(self, app_name, log_errors=True): """Start an app by name""" # sometimes device does not start app if already running one self.home() - url="{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) - data="LOCATION: {0}/run".format(url) - self.send_http(url, HttpMethod.POST, data=data) + url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) + data = "LOCATION: {0}/run".format(url) + self._send_http(url, HttpMethod.POST, data=data) pass def send_command(self, name): @@ -545,14 +535,8 @@ def send_command(self, name): def get_action(self, name): if not name in self.actions and len(self.actions) == 0: - self.update_service_urls() + self._update_service_urls() if not name in self.actions and len(self.actions) == 0: - if name == "register": - registration_action = XmlApiObject(None) - registration_action.url = "http://{0}/sony/accessControl".format( - self.host) - registration_action.mode = 4 - return registration_action raise ValueError('Failed to read action list from device.') return self.actions[name] @@ -714,21 +698,19 @@ def list(self): self.send_command('List') - if __name__ == "__main__": - stored_config="bluray.json" - device=None + stored_config = "bluray.json" + device = None # device must be on for registration - host="192.168.178.23" - device=SonyDevice(host, "SonyApiLib Python Test10") + host = "192.168.178.23" + device = SonyDevice(host, "SonyApiLib Python Test10") device.register() - pin=input("Enter the PIN displayed at your device: ") + pin = input("Enter the PIN displayed at your device: ") device.send_authentication(pin) # save_device() - - apps=device.get_apps() + apps = device.get_apps() device.start_app(apps[0]) From dfaa84bbf4fc7ca633830833e248fd46f5f5cbf3 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 1 Apr 2019 22:19:53 +0200 Subject: [PATCH 040/170] Updated Version number --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 01ad8d3..4453bb6 100644 --- a/setup.py +++ b/setup.py @@ -15,12 +15,12 @@ # and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" setup(name='sonyapilib', packages = ['sonyapilib'], # this must be the same as the name above - version = '0.3.10', + version = '0.3.11', description = 'Lib to control sony devices with their soap api', author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', url = 'https://github.com/alexmohr/sonyapilib', # use the URL to the github repo - download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3.10', + download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3.11', keywords = ['soap', 'sony', 'api'], # arbitrary keywords classifiers = [], install_requires=[ From 7c02a8c9927218c934c926ebaedada9f39bead63 Mon Sep 17 00:00:00 2001 From: dilruacs Date: Tue, 2 Apr 2019 11:17:18 +0200 Subject: [PATCH 041/170] Update README.md - Sony component has its own repo now - Fixed Typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7345387..fd16ff0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Sonyapilib Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant wich you can find here: -https://github.com/dilruacs/home-assistant-custom-components/tree/master/custom_components/sony +https://github.com/dilruacs/media_player.sony # Installation ``` pip install sonyapilib @@ -49,7 +49,7 @@ if __name__ == "__main__": device.play() ``` -# Compatability List +# Compatibility List LCD TV BRAVIA 2016 model or later: From b411737e4f11c92371077a7a223c6160ecd45f03 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Apr 2019 20:15:07 +0200 Subject: [PATCH 042/170] Refactoring and added travis. --- .travis.yml | 8 ++ .vscode/launch.json | 2 +- sonyapilib/device.py | 296 ++++++++++++++++++++++--------------------- tests/deviceTest.py | 57 +++++++++ tests/dmr_v3.xml | 74 +++++++++++ tests/dmr_v4.xml | 165 ++++++++++++++++++++++++ tests/ircc.xml | 74 +++++++++++ 7 files changed, 528 insertions(+), 148 deletions(-) create mode 100644 .travis.yml create mode 100644 tests/deviceTest.py create mode 100644 tests/dmr_v3.xml create mode 100644 tests/dmr_v4.xml create mode 100644 tests/ircc.xml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..9bff6f4 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: python +python: + - "3.7" +cache: pip +install: + - pip install -r requirements.txt +script: + - python tests/deviceTest.py \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json index 37d6afd..35bc23c 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "Python: Current File", "type": "python", "request": "launch", - "stopOnEntry": true, + "stopOnEntry": false, "pythonPath": "${config:python.pythonPath}", "program": "${file}", "cwd": "${workspaceFolder}", diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 87e3930..ec544a4 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -22,7 +22,7 @@ TIMEOUT = 5 URN_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}" URN_SONY_AV = "{urn:schemas-sony-com:av}" - +URN_SCALAR_WEB_API_DEVICE_INFO = "{urn:schemas-sony-com:av}" class AuthenticationResult(Enum): @@ -89,8 +89,10 @@ def __init__(self, host, nickname): self.is_v4 = False self.ircc_url = "http://{0}:{1}/Ircc.xml".format(host, self.ircc_port) - self.irccscpd_url = "http://{0}:{1}/IRCCSCPD.xml".format(host, self.ircc_port) - self.dmr_url = "http://{0}:{1}/dmr.xml".format(self.host, self.dmr_port) + self.irccscpd_url = "http://{0}:{1}/IRCCSCPD.xml".format( + host, self.ircc_port) + self.dmr_url = "http://{0}:{1}/dmr.xml".format( + self.host, self.dmr_port) self.app_url = "http://{0}:{1}".format(self.host, self.app_port) @staticmethod @@ -115,37 +117,31 @@ def save_to_json(self): """ Save this device configuration into a json """ return jsonpickle.dumps(self) - def wakeonlan(self): - if self.mac is not None: - wakeonlan.send_magic_packet(self.mac, ip_address=self.host) - def _update_service_urls(self): """ Initialize the device by reading the necessary resources from it """ - self._parse_dmr() - - self.recreate_authentication() - if self.is_v4: - self._parse_irccscpd() - else: - self._parse_ircc() + response = self._send_http(self.dmr_url, method=HttpMethod.GET) + if not response: + _LOGGER.error("Failed to get DMR") + return - if len(self.commands) > 0: - self.update_commands() - self.update_applist() + self._parse_dmr(response.text) - def _parse_irccscpd(self): - response = self._send_http(self.irccscpd_url, method=HttpMethod.GET) - if response is None: - return + self._recreate_authentication() + if not self.is_v4: + response = self._send_http(self.ircc_url, method=HttpMethod.GET) + self._parse_ircc(response.text) - def _parse_ircc(self): + if len(self.commands) > 0: + self._update_commands() + self._update_applist() + + def _parse_ircc(self, data): response = self._send_http(self.ircc_url, method=HttpMethod.GET) - if response is None: + if not response: return - raw_data = response.text - xml_data = xml.etree.ElementTree.fromstring(raw_data) - + xml_data = xml.etree.ElementTree.fromstring(data) + # the action list contains everything the device supports self.actionlist_url = xml_data.find("{0}device".format(URN_UPNP_DEVICE))\ .find("{0}X_UNR_DeviceInfo".format(URN_SONY_AV))\ @@ -158,7 +154,7 @@ def _parse_ircc(self): # read action list response = self._send_http(self.actionlist_url, method=HttpMethod.GET) - if response is not None: + if not response: raw_data = response.text xml_data = xml.etree.ElementTree.fromstring(raw_data) for element in xml_data.findall("action"): @@ -169,18 +165,19 @@ def _parse_ircc(self): if action.name == "register": if action.mode < 4: # the authentication later on is based on the device id and the mac + # todo maybe refactor this to requests http://docs.python-requests.org/en/master/_modules/requests/api/?highlight=param action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format( action.url, urllib.parse.quote(self.nickname)) if action.mode == 3: action.url = action.url + "&wolSupport=true" - + lirc_url = urllib.parse.urlparse(self.ircc_url) - if services is not None: + if services: # read service list for service in services: service_id = service.find( "{0}serviceId".format(URN_UPNP_DEVICE)) - if service_id == None: + if not service_id: continue if "urn:schemas-sony-com:serviceId:IRCC" not in service_id.text: continue @@ -203,14 +200,9 @@ def _parse_ircc(self): self.mac = function.find( "functionItem").attrib["value"] - def _parse_dmr(self): - # get control data for sending commands - response = self._send_http(self.dmr_url, method=HttpMethod.GET) - if response is None: - return - + def _parse_dmr(self, data): lirc_url = urllib.parse.urlparse(self.ircc_url) - xml_data = xml.etree.ElementTree.fromstring(response.text) + xml_data = xml.etree.ElementTree.fromstring(data) for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): serviceList = device.find( "{0}serviceList".format(URN_UPNP_DEVICE)) @@ -223,35 +215,41 @@ def _parse_dmr(self): "{0}controlURL".format(URN_UPNP_DEVICE)).text self.av_transport_url = "{0}://{1}:{2}{3}".format( lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) - + # this is only for v4 devices. - if not "av:X_ScalarWebAPI_ServiceType" in response.text: + if not "av:X_ScalarWebAPI_ServiceType" in data: return self.is_v4 = True + deviceInfo = "{0}X_ScalarWebAPI_DeviceInfo"\ + .format(URN_SCALAR_WEB_API_DEVICE_INFO) + for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): - serviceList = device.find("{0}serviceList".format(URN_UPNP_DEVICE)) - for service in serviceList: - service_id = service.find( - "{0}serviceId".format(URN_UPNP_DEVICE)) - if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: - continue - transport_location = service.find( - "{0}controlURL".format(URN_UPNP_DEVICE)).text - self.av_transport_url = "{0}://{1}:{2}{3}".format( - lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + for deviceInfo in device.findall(deviceInfo): + base_url = deviceInfo.find("{0}X_ScalarWebAPI_BaseURL"\ + .format(URN_SCALAR_WEB_API_DEVICE_INFO))\ + .text + + action = XmlApiObject(None) + action.url = base_url + "/accessControl" + action.mode = 4 + self.actions["register"] = action + action = XmlApiObject(None) + action.url = base_url + "/system" + self.actions["getRemoteCommandList"] = action + - def update_commands(self): + def _update_commands(self): # needs to be registered to do that - if self.pin is None: + if not self.pin: return url = self.get_action("getRemoteCommandList").url if self.get_action("register").mode < 4: response = self._send_http(url, method=HttpMethod.GET) - if response is not None: + if response: xml_data = xml.etree.ElementTree.fromstring(response.text) for command in xml_data.findall("command"): @@ -260,8 +258,9 @@ def update_commands(self): else: response = self._send_http( url, method=HttpMethod.POST, data=self._create_api_json("getRemoteControllerInfo", 1)) - if response is not None: + if response: json = response.json() + # todo parse json if not json.get('error'): # todo this does not fit 100% with the structure of this lib. # see github issue#2 @@ -270,10 +269,10 @@ def update_commands(self): _LOGGER.error("JSON request error: " + json.dumps(json, indent=4)) - def update_applist(self, log_errors=True): + def _update_applist(self, log_errors=True): url = self.app_url + "/appslist" response = self._send_http(url, method=HttpMethod.GET) - if response is not None: + if response: xml_data = xml.etree.ElementTree.fromstring(response.text) apps = xml_data.findall(".//app") for app in apps: @@ -284,12 +283,12 @@ def update_applist(self, log_errors=True): data.id = id self.apps[name] = data - def recreate_authentication(self): + def _recreate_authentication(self): """ The default cookie is for URL/sony. For some commands we need it for the root path. """ - if self.pin == None: + if not self.pin: return # todo fix cookies @@ -311,73 +310,6 @@ def recreate_authentication(self): return cookies - def register(self): - """ - Register at the api. The name which will be displayed in the UI of the device. Make sure this name does not exist yet - For this the device must be put in registration mode. - The tested sd5500 has no separte mode but allows registration in the overview " - """ - registration_result = AuthenticationResult.ERROR - - registration_action = registration_action = self.get_action("register") - - # protocol version 1 and 2 - if registration_action.mode < 3: - registration_response = self._send_http( - registration_action.url, method=HttpMethod.GET, raise_errors=True) - registration_result = AuthenticationResult.SUCCESS - - # protocol version 3 - elif registration_action.mode == 3: - try: - self._send_http(registration_action.url, - method=HttpMethod.GET, raise_errors=True) - except requests.exceptions.HTTPError as ex: - _LOGGER.error("[W] HTTPError: " + str(ex)) - # todo set the correct result. - registration_result = AuthenticationResult.PIN_NEEDED - - # newest protocol version 4 this is the same method as braviarc uses - elif registration_action.mode == 4: - authorization = self._create_api_json("actRegister", 13) - - try: - headers = { - "Content-Type": "application/json" - } - response = self._send_http(registration_action.url, method=HttpMethod.POST, headers=headers, - data=authorization, raise_errors=True) - except requests.exceptions.HTTPError as ex: - _LOGGER.error("[W] HTTPError: " + str(ex)) - # todo set the correct result. - registration_result = AuthenticationResult.PIN_NEEDED - - except Exception as ex: # pylint: disable=broad-except - _LOGGER.error("[W] Exception: " + str(ex)) - else: - resp = response.json() - _LOGGER.debug(json.dumps(resp, indent=4)) - if resp is None or not resp.get('error'): - self.cookies = response.cookies - registration_result = AuthenticationResult.SUCCESS - - else: - raise ValueError( - "Regisration mode {0} is not supported".format(registration_action.mode)) - - return registration_result - - def send_authentication(self, pin): - registration_action = self.get_action("register") - - # they do not need a pin - if registration_action.mode < 3: - return True - - self.pin = pin - self.recreate_authentication() - self.register() - def _request_json(self, url, params, log_errors=True): """ Send request command via HTTP json to Sony Bravia.""" built_url = 'http://{}/{}'.format(self.host, url) @@ -388,7 +320,7 @@ def _request_json(self, url, params, log_errors=True): def _create_api_json(self, method, id, params=None): """ Create json data which will be send via post for the V4 api""" - if params is None: + if not params: params = [{ "clientid": self.get_device_id(), "nickname": self.nickname @@ -414,10 +346,10 @@ def _create_api_json(self, method, id, params=None): def _send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): """ Send request command via HTTP json to Sony Bravia.""" - if headers is None: + if not headers: headers = self.headers - if url is None: + if not url: return _LOGGER.debug( @@ -425,7 +357,7 @@ def _send_http(self, url, method, data=None, headers=None, log_errors=True, rais try: params = "" - if data is not None: + if data: params = data.encode("UTF-8") if method == HttpMethod.POST: @@ -455,7 +387,7 @@ def _send_http(self, url, method, data=None, headers=None, log_errors=True, rais else: return response - def post_soap_request(self, url, params, action): + def _post_soap_request(self, url, params, action): headers = { 'SOAPACTION': '"{0}"'.format(action), "Content-Type": "text/xml" @@ -469,10 +401,10 @@ def post_soap_request(self, url, params, action): "" response = self._send_http( url, method=HttpMethod.POST, headers=headers, data=data) - if response is not None: + if response: return response.content.decode("utf-8") - def send_req_ircc(self, params, log_errors=True): + def _send_req_ircc(self, params, log_errors=True): """Send an IRCC command via HTTP to Sony Bravia.""" data = "" +\ @@ -480,13 +412,84 @@ def send_req_ircc(self, params, log_errors=True): "" action = "urn:schemas-sony-com:service:IRCC:1#X_SendIRCC" - content = self.post_soap_request( + content = self._post_soap_request( url=self.control_url, params=data, action=action) return content def get_device_id(self): return "TVSideView:{0}".format(self.mac) + def register(self): + """ + Register at the api. The name which will be displayed in the UI of the device. Make sure this name does not exist yet + For this the device must be put in registration mode. + The tested sd5500 has no separte mode but allows registration in the overview " + """ + registration_result = AuthenticationResult.ERROR + + registration_action = registration_action = self.get_action("register") + + # protocol version 1 and 2 + if registration_action.mode < 3: + registration_response = self._send_http( + registration_action.url, method=HttpMethod.GET, raise_errors=True) + registration_result = AuthenticationResult.SUCCESS + + # protocol version 3 + elif registration_action.mode == 3: + try: + self._send_http(registration_action.url, + method=HttpMethod.GET, raise_errors=True) + except requests.exceptions.HTTPError as ex: + _LOGGER.error("[W] HTTPError: " + str(ex)) + # todo set the correct result. + registration_result = AuthenticationResult.PIN_NEEDED + + # newest protocol version 4 this is the same method as braviarc uses + elif registration_action.mode == 4: + authorization = self._create_api_json("actRegister", 13) + + try: + headers = { + "Content-Type": "application/json" + } + response = self._send_http(registration_action.url, method=HttpMethod.POST, headers=headers, + data=authorization, raise_errors=True) + except requests.exceptions.HTTPError as ex: + _LOGGER.error("[W] HTTPError: " + str(ex)) + # todo set the correct result. + registration_result = AuthenticationResult.PIN_NEEDED + + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error("[W] Exception: " + str(ex)) + else: + resp = response.json() + _LOGGER.debug(json.dumps(resp, indent=4)) + if not resp or not resp.get('error'): + self.cookies = response.cookies + registration_result = AuthenticationResult.SUCCESS + + else: + raise ValueError( + "Regisration mode {0} is not supported".format(registration_action.mode)) + + return registration_result + + def send_authentication(self, pin): + registration_action = self.get_action("register") + + # they do not need a pin + if registration_action.mode < 3: + return True + + self.pin = pin + self._recreate_authentication() + self.register() + + def wakeonlan(self): + if self.mac: + wakeonlan.send_magic_packet(self.mac, ip_address=self.host) + def get_playing_status(self): data = '' + \ '0' + \ @@ -494,9 +497,9 @@ def get_playing_status(self): action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" - content = self.post_soap_request( + content = self._post_soap_request( url=self.av_transport_url, params=data, action=action) - if None is content: + if not content: return "OFF" response = xml.etree.ElementTree.fromstring(content) state = response.find(".//CurrentTransportState").text @@ -512,22 +515,13 @@ def get_power_status(self): return False return True - def start_app(self, app_name, log_errors=True): - """Start an app by name""" - # sometimes device does not start app if already running one - self.home() - url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) - data = "LOCATION: {0}/run".format(url) - self._send_http(url, HttpMethod.POST, data=data) - pass - def send_command(self, name): if len(self.commands) == 0: - self.update_commands() + self._update_commands() if len(self.commands) > 0: if name in self.commands: - self.send_req_ircc(self.commands[name].value) + self._send_req_ircc(self.commands[name].value) else: raise ValueError('Unknown command: %s', name) else: @@ -541,13 +535,21 @@ def get_action(self, name): return self.actions[name] + def start_app(self, app_name, log_errors=True): + """Start an app by name""" + # sometimes device does not start app if already running one + self.home() + url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) + data = "LOCATION: {0}/run".format(url) + self._send_http(url, HttpMethod.POST, data=data) + def power(self, on): - if (on): + if on: self.wakeonlan() if not self.get_power_status(): + # Try using the power on command incase the WOL doesn't work self.send_command('Power') else: - # Try using the power on command incase the WOL doesn't work self.send_command('Power') def get_apps(self): diff --git a/tests/deviceTest.py b/tests/deviceTest.py new file mode 100644 index 0000000..6c69a92 --- /dev/null +++ b/tests/deviceTest.py @@ -0,0 +1,57 @@ +import unittest +import os.path + +from inspect import getsourcefile +import os.path as path, sys +current_dir = path.dirname(path.abspath(getsourcefile(lambda:0))) +sys.path.insert(0, current_dir[:current_dir.rfind(path.sep)]) +from sonyapilib.device import SonyDevice +sys.path.pop(0) + +class TestHelper: + + @staticmethod + def read_file(file_name): + """ Reads a file from disk """ + __location__ = os.path.realpath(os.path.join( + os.getcwd(), os.path.dirname(__file__))) + with open(os.path.join(__location__, file_name)) as f: + return f.read() + + +class SonyDeviceTest(unittest.TestCase): + + def test_parse_dmr_v3(self): + content = TestHelper.read_file("dmr_v3.xml") + device = self.create_device() + device._parse_dmr(content) + self.verify_device_dmr(device) + self.assertFalse(device.is_v4) + + def test_parse_dmr_v4(self): + content = TestHelper.read_file("dmr_v4.xml") + device = self.create_device() + device._parse_dmr(content) + self.verify_device_dmr(device) + self.assertTrue(device.is_v4) + self.assertEqual(device.actions["register"].url, + 'http://192.168.178.23/sony/accessControl') + self.assertEqual(device.actions["register"].mode, 4) + self.assertEqual(device.actions["getRemoteCommandList"].url, + 'http://192.168.178.23/sony/system') + + def test_parse_ircc(self): + content = TestHelper.read_file("ircc.xml") + device = self.create_device() + device._parse_ircc(content) + + def create_device(self): + return SonyDevice("test", "test") + + def verify_device_dmr(self, device): + self.assertEqual(device.av_transport_url, + 'http://test:52323/upnp/control/AVTransport') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/dmr_v3.xml b/tests/dmr_v3.xml new file mode 100644 index 0000000..d3b0d61 --- /dev/null +++ b/tests/dmr_v3.xml @@ -0,0 +1,74 @@ + + + 1 + 0 + + + urn:schemas-upnp-org:device:MediaRenderer:1 + + Sony Corporation + http://www.sony.net/ + BDP-S5500 + BDP-2015 + uuid:00000000-0000-1010-8000-ac9b0aed6635 + DMR-1.50 + playcontainer-0-0 + + + image/jpeg + 120 + 120 + 24 + /bdp_ax_device_icon_large.jpg + + + image/png + 120 + 120 + 24 + /bdp_ax_device_icon_large.png + + + image/jpeg + 48 + 48 + 24 + /bdp_ax_device_icon_small.jpg + + + image/png + 48 + 48 + 24 + /bdp_ax_device_icon_small.png + + + + + urn:schemas-upnp-org:service:RenderingControl:1 + urn:upnp-org:serviceId:RenderingControl + /RenderingControlBdpSCPD.xml + /upnp/control/RenderingControl + /upnp/event/RenderingControl + + + urn:schemas-upnp-org:service:ConnectionManager:1 + urn:upnp-org:serviceId:ConnectionManager + /ConnectionManagerSCPD.xml + /upnp/control/ConnectionManager + /upnp/event/ConnectionManager + + + urn:schemas-upnp-org:service:AVTransport:1 + urn:upnp-org:serviceId:AVTransport + /AVTransportBdpSCPD.xml + /upnp/control/AVTransport + /upnp/event/AVTransport + + + 1.1 + 1 + + \ No newline at end of file diff --git a/tests/dmr_v4.xml b/tests/dmr_v4.xml new file mode 100644 index 0000000..d8a8274 --- /dev/null +++ b/tests/dmr_v4.xml @@ -0,0 +1,165 @@ + + + + 1 + 0 + + + urn:schemas-upnp-org:device:MediaRenderer:1 + BRAVIA KDL-32W706B + Sony Corporation + http://www.sony.net/ + KDL-32W706B + MINT1.7.0.1 + uuid:00000000-0000-1010-8000-fcf152cb2523 + DMR-1.50 + 1 + + + image/png + 32 + 32 + 24 + /MediaRenderer_HE_L_32x32.png + + + image/png + 48 + 48 + 24 + /MediaRenderer_HE_L_48x48.png + + + image/png + 60 + 60 + 24 + /MediaRenderer_HE_L_60x60.png + + + image/png + 120 + 120 + 24 + /MediaRenderer_HE_L_120x120.png + + + image/jpeg + 32 + 32 + 24 + /MediaRenderer_HE_L_32x32.jpg + + + image/jpeg + 48 + 48 + 24 + /MediaRenderer_HE_L_48x48.jpg + + + image/jpeg + 60 + 60 + 24 + /MediaRenderer_HE_L_60x60.jpg + + + image/jpeg + 120 + 120 + 24 + /MediaRenderer_HE_L_120x120.jpg + + + + + urn:schemas-upnp-org:service:RenderingControl:1 + urn:upnp-org:serviceId:RenderingControl + /RenderingControlSCPD.xml + /upnp/control/RenderingControl + /upnp/event/RenderingControl + + + urn:schemas-upnp-org:service:ConnectionManager:1 + urn:upnp-org:serviceId:ConnectionManager + /ConnectionManagerSCPD.xml + /upnp/control/ConnectionManager + /upnp/event/ConnectionManager + + + urn:schemas-upnp-org:service:AVTransport:1 + urn:upnp-org:serviceId:AVTransport + /AVTransportSCPD.xml + /upnp/control/AVTransport + /upnp/event/AVTransport + + + urn:dial-multiscreen-org:service:dial:1 + urn:dial-multiscreen-org:serviceId:dial + /DIALSCPD.xml + /upnp/control/DIAL + + + + urn:schemas-sony-com:service:IRCC:1 + urn:schemas-sony-com:serviceId:IRCC + /IRCCSCPD.xml + http://192.168.178.23/sony/IRCC + + + + urn:schemas-sony-com:service:ScalarWebAPI:1 + urn:schemas-sony-com:serviceId:ScalarWebAPI + /ScalarWebApiSCPD.xml + /upnp/control/ScalarAPI + + + + 64 + 1.1 + 2K + + AAAAAQAAAAEAAAAVAw== + AAAAAQAAAAEAAAAuAw== + AAAAAQAAAAEAAAAvAw== + + MS_DigitalMediaDeviceClass_DMR_V001 + MediaDevices + VEN_0106&DEV_0007&REV_01 + Display.TV Multimedia.DMR + + http://192.168.178.23/sony/BgmSearch + + + 1.0 + http://192.168.178.23/sony + + guide + system + videoScreen + audio + avContent + recording + appControl + browser + notification + cec + accessControl + encryption + + + + 1.0 + false + 20677 + + + http://192.168.178.23/DIAL/sony/applist + B0:00:03:3F:5A:DE + CoreTV_DIAL + + + \ No newline at end of file diff --git a/tests/ircc.xml b/tests/ircc.xml new file mode 100644 index 0000000..bbc5074 --- /dev/null +++ b/tests/ircc.xml @@ -0,0 +1,74 @@ + + + + 1 + 0 + + + urn:schemas-upnp-org:device:Basic:1 + Blu-ray Disc Player + Sony Corporation + http://www.sony.net/ + + Blu-ray Disc Player + + uuid:00000003-0000-1010-8000-fcf1524e7a1e + + + image/jpeg + 120 + 120 + 24 + /bdp_ax3d_device_icon_large.jpg + + + image/png + 120 + 120 + 24 + /bdp_ax3d_device_icon_large.png + + + image/jpeg + 48 + 48 + 24 + /bdp_ax3d_device_icon_small.jpg + + + image/png + 48 + 48 + 24 + /bdp_ax3d_device_icon_small.png + + + + + urn:schemas-sony-com:service:IRCC:1 + urn:schemas-sony-com:serviceId:IRCC + /IRCCSCPD.xml + /upnp/control/IRCC + + + + + + 1.0 + + + AAMAABxa + + + + + 1.3 + http://192.168.240.4:50002/actionList + + + 1.0 + false + 50004 + + + \ No newline at end of file From 2525bd7c15872ba49aead0b25f36ec0b8db77e48 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Apr 2019 22:26:24 +0200 Subject: [PATCH 043/170] Changed python version for travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 9bff6f4..a02c4e1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "3.7" + - "3.6" cache: pip install: - pip install -r requirements.txt From bec625d6832cef0cb384c1637a82a32f8585a971 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Apr 2019 22:33:45 +0200 Subject: [PATCH 044/170] mend --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index a02c4e1..cd7f836 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ python: - "3.6" cache: pip install: - - pip install -r requirements.txt + - pip install . --user script: - python tests/deviceTest.py \ No newline at end of file From c6f2efb10a4428fea5874a351a83e3b9a359c10a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Apr 2019 22:35:21 +0200 Subject: [PATCH 045/170] mend --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index cd7f836..1642543 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,6 @@ python: - "3.6" cache: pip install: - - pip install . --user + - pip install . script: - python tests/deviceTest.py \ No newline at end of file From 215813af3e8a37ed44855f45b1aebb5816202ac6 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Tue, 2 Apr 2019 22:35:42 +0200 Subject: [PATCH 046/170] Call update_service_urls() after successfull registration --- sonyapilib/device.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 8b37211..4363265 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -225,9 +225,8 @@ def update_service_urls(self): self.av_transport_url = "{0}://{1}:{2}{3}".format( lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) - if len(self.commands) > 0: - self.update_commands() - self.update_applist() + self.update_commands() + self.update_applist() def update_commands(self): @@ -301,10 +300,10 @@ def recreate_authentication(self): def register(self): """ - Register at the api.50001 - Register at the api. The name which will be displayed in the UI of the device. Make sure this name does not exist yet + Register at the api. + The name which will be displayed in the UI of the device. Make sure this name does not exist yet. For this the device must be put in registration mode. - The tested sd5500 has no separte mode but allows registration in the overview " + The tested sd5500 has no separate mode but allows registration in the overview " """ registration_result = AuthenicationResult.ERROR registration_action = registration_action = self.get_action("register") @@ -359,7 +358,10 @@ def register(self): else: raise ValueError( - "Regisration mode {0} is not supported".format(registration_action.mode)) + "Registration mode {0} is not supported".format(registration_action.mode)) + + if registration_result == AuthenicationResult.SUCCESS: + self.update_service_urls() return registration_result From 8e9ec0da61e5c9a475eb0d76c3bb60979c457c3b Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Apr 2019 22:49:39 +0200 Subject: [PATCH 047/170] Changed init. --- sonyapilib/device.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 5ad2875..10e9a4a 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -93,7 +93,12 @@ def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, i self.app_url = "http://{0}:{1}".format(self.host, self.app_port) if len(self.actions) == 0 and self.pin is not None: - self.update_service_urls() + self.init_device() + + def init_device(self): + self.update_service_urls() + self.update_commands + @staticmethod def discover(): @@ -369,6 +374,7 @@ def send_authentication(self, pin): # they do not need a pin if registration_action.mode < 3: + self.init_device() return True self.pin = pin @@ -382,9 +388,6 @@ def send_authentication(self, pin): return False else: self.pin = pin - return True - return False - elif registration_action.mode == 4: authorization = json.dumps( { @@ -419,8 +422,12 @@ def send_authentication(self, pin): if resp is None or not resp.get('error'): self.cookies = response.cookies self.pin = pin - return True - return False + else: + return False + + self.init_device() + return True + def send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): """ Send request command via HTTP json to Sony Bravia.""" From accf3e85f36797f9ead2356f6382cd8c20574f13 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Apr 2019 22:50:32 +0200 Subject: [PATCH 048/170] updated init --- sonyapilib/device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 10e9a4a..f34d7fb 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -97,7 +97,8 @@ def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, i def init_device(self): self.update_service_urls() - self.update_commands + self.update_commands() + self.update_applist() @staticmethod From 75873171b9955b55462430d81c6f9c9dfd594561 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Apr 2019 22:52:06 +0200 Subject: [PATCH 049/170] Decoupled methods --- sonyapilib/device.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index f34d7fb..fb734a2 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -231,9 +231,7 @@ def update_service_urls(self): self.av_transport_url = "{0}://{1}:{2}{3}".format( lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) - if len(self.commands) > 0: - self.update_commands() - self.update_applist() + self.init_device() def update_commands(self): From e26bc1c8434fbf67a8b38423b845969760232af8 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 2 Apr 2019 22:55:49 +0200 Subject: [PATCH 050/170] Removed recursive call --- sonyapilib/device.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index fb734a2..68932fa 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -231,8 +231,6 @@ def update_service_urls(self): self.av_transport_url = "{0}://{1}:{2}{3}".format( lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) - self.init_device() - def update_commands(self): # needs to be registred to do that From 1dc41b8b7df5afaf90cc3b4d058f8be8d180e627 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Wed, 3 Apr 2019 11:22:16 +0200 Subject: [PATCH 051/170] Update device URL's and command list after authentication has been send --- sonyapilib/device.py | 34 ++++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 1bfde46..3acc793 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -341,8 +341,10 @@ def register(self): ).encode('utf-8') try: - response = self.send_http(registration_action.url, method=HttpMethod.POST, - data=authorization, raise_errors=True) + response = self.send_http(registration_action.url, + method=HttpMethod.POST, + data=authorization, + raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) registration_result = AuthenticationResult.PIN_NEEDED @@ -360,9 +362,6 @@ def register(self): raise ValueError( "Registration mode {0} is not supported".format(registration_action.mode)) - if registration_result == AuthenicationResult.SUCCESS: - self.update_service_urls() - return registration_result def send_authentication(self, pin): @@ -379,14 +378,14 @@ def send_authentication(self, pin): if registration_action.mode == 3: try: self.send_http( - self.get_action("register").url, method=HttpMethod.GET, raise_errors=True) - except: + self.get_action("register").url, + method=HttpMethod.GET, + raise_errors=True) + # What exception are we trying to catch here? + except Exception: return False else: self.pin = pin - return True - return False - elif registration_action.mode == 4: authorization = json.dumps( { @@ -411,9 +410,11 @@ def send_authentication(self, pin): ) try: - response = self.send_http(self.get_action("register").url, method=HttpMethod.post, - data=authorization, raise_errors=True) - except: + response = self.send_http(self.get_action("register").url, + method=HttpMethod.post, + data=authorization, + raise_errors=True) + except Exception: return False else: resp = response.json() @@ -421,8 +422,13 @@ def send_authentication(self, pin): if resp is None or not resp.get('error'): self.cookies = response.cookies self.pin = pin - return True + # Authentication was sent and no error occurred + # update URL's and Command array + try: + self.update_service_urls() + except Exception: return False + return True def send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): """ Send request command via HTTP json to Sony Bravia.""" From 6aacbef8e2ff77a89cd17a9316917b1ecb4ae92d Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Wed, 3 Apr 2019 11:59:50 +0200 Subject: [PATCH 052/170] Small style fixes --- sonyapilib/device.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 68932fa..9fd36d5 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -303,10 +303,13 @@ def recreate_authentication(self): def register(self): """ - Register at the api.50001 - Register at the api. The name which will be displayed in the UI of the device. Make sure this name does not exist yet + Register at the api. + + The name which will be displayed in the UI of the device. + Make sure this name does not exist yet. For this the device must be put in registration mode. - The tested sd5500 has no separte mode but allows registration in the overview " + The tested sd5500 has no separte mode but allows registration in the + overview """ registration_result = AuthenticationResult.ERROR registration_action = registration_action = self.get_action("register") @@ -361,7 +364,7 @@ def register(self): else: raise ValueError( - "Regisration mode {0} is not supported".format(registration_action.mode)) + "Registration mode {0} is not supported".format(registration_action.mode)) return registration_result @@ -409,8 +412,10 @@ def send_authentication(self, pin): ) try: - response = self.send_http(self.get_action("register").url, method=HttpMethod.post, - data=authorization, raise_errors=True) + response = self.send_http(self.get_action("register").url, + method=HttpMethod.post, + data=authorization, + raise_errors=True) except: return False else: @@ -421,10 +426,10 @@ def send_authentication(self, pin): self.pin = pin else: return False - + self.init_device() return True - + def send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): """ Send request command via HTTP json to Sony Bravia.""" From 6e072b0dcfb48ad187a5928e8fe3ef5aad330886 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Wed, 3 Apr 2019 13:35:29 +0200 Subject: [PATCH 053/170] Fix linter errors --- sonyapilib/device.py | 107 ++++++++++++++++++++++--------------------- 1 file changed, 54 insertions(+), 53 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 9fd36d5..ae6f2d8 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -1,17 +1,11 @@ -""" -Sony Mediaplayer lib -""" +"""Sony Mediaplayer lib""" import logging import base64 -import collections import json -import socket -import struct -import requests import urllib.parse import xml.etree.ElementTree -import requests from enum import Enum +import requests import wakeonlan import jsonpickle @@ -29,7 +23,7 @@ class AuthenticationResult(Enum): class HttpMethod(Enum): - GET = 0, + GET = 0 POST = 1 @@ -50,17 +44,16 @@ def __init__(self, xml_data): if "_" in arg: continue if arg in xml_data: - if (arg == "mode"): + if arg == "mode": setattr(self, arg, int(xml_data[arg])) else: setattr(self, arg, xml_data[arg]) class SonyDevice(): - """ - Contains all data for the device - """ + """Contains all data for the device.""" - def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, ircc_location=None): + def __init__(self, host, nickname, port=50001, dmr_port=52323, + app_port=50202, ircc_location=None): """ Init the device with the entry point""" self.host = host self.nickname = nickname @@ -83,7 +76,7 @@ def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, i self.mac = None self.authenticated = False - if self.ircc_url == None: + if self.ircc_url is None: self.ircc_url = "http://{0}:{1}/Ircc.xml".format(host, port) self.dmr_port = dmr_port @@ -92,7 +85,7 @@ def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, i self.host, self.dmr_port) self.app_url = "http://{0}:{1}".format(self.host, self.app_port) - if len(self.actions) == 0 and self.pin is not None: + if not self.actions and self.pin is not None: self.init_device() def init_device(self): @@ -100,7 +93,6 @@ def init_device(self): self.update_commands() self.update_applist() - @staticmethod def discover(): """ @@ -116,7 +108,7 @@ def discover(): @staticmethod def load_from_json(data): - """ Loads a device configuration from a stored json """ + """Load a device configuration from a stored json.""" return jsonpickle.decode(data) def save_to_json(self): @@ -124,10 +116,10 @@ def save_to_json(self): return jsonpickle.dumps(self) def create_json_v4(self, method, params=None): - """ Create json data which will be send via post for the V4 api""" + """Create json data which will be send via post for the V4 api.""" if params is not None: - ret = json.dumps({"method": method, "params": [ - params], "id": 1, "version": "1.0"}) + ret = json.dumps({"method": method, "params": [params], + "id": 1, "version": "1.0"}) else: ret = json.dumps({"method": method, "params": [], "id": 1, "version": "1.0"}) @@ -138,7 +130,7 @@ def wakeonlan(self): wakeonlan.send_magic_packet(self.mac, ip_address=self.host) def update_service_urls(self): - """ Initalizes the device by reading the necessary resources from it """ + """Initalize the device by reading the necessary resources from it.""" lirc_url = urllib.parse.urlparse(self.ircc_url) response = self.send_http(self.ircc_url, method=HttpMethod.GET) @@ -174,7 +166,7 @@ def update_service_urls(self): if action.mode < 4: # the authenication later on is based on the device id and the mac action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format( - action.url, urllib.parse.quote(self.nickname)) + action.url, urllib.parse.quote(self.nickname)) if action.mode == 3: action.url = action.url + "&wolSupport=true" elif action.mode == 4: @@ -192,7 +184,7 @@ def update_service_urls(self): # read service list for service in services: service_id = service.find("{0}serviceId".format(urn_upnp_device)) - if service_id == None: + if service_id is None: continue if "urn:schemas-sony-com:serviceId:IRCC" not in service_id.text: continue @@ -229,7 +221,8 @@ def update_service_urls(self): transport_location = service.find( "{0}controlURL".format(urn_upnp_device)).text self.av_transport_url = "{0}://{1}:{2}{3}".format( - lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + lirc_url.scheme, lirc_url.netloc.split(":")[0], + self.dmr_port, transport_location) def update_commands(self): @@ -248,7 +241,8 @@ def update_commands(self): self.commands[name] = XmlApiObject(command.attrib) else: response = self.send_http( - url, method=HttpMethod.POST, data=self.create_json_v4("getRemoteControllerInfo")) + url, method=HttpMethod.POST, + data=self.create_json_v4("getRemoteControllerInfo")) if response is not None: json = response.json() if not json.get('error'): @@ -257,9 +251,9 @@ def update_commands(self): self.commands = json.get('result')[1] else: _LOGGER.error("JSON request error: " + - json.dumps(json, indent=4)) + json.dumps(json, indent=4)) - def update_applist(self, log_errors=True): + def update_applist(self): url = self.app_url + "/appslist" response = self.send_http(url, method=HttpMethod.GET) if response is not None: @@ -275,21 +269,22 @@ def update_applist(self, log_errors=True): def recreate_authentication(self): """ - The default cookie is for URL/sony. For some commands we need it for the root path. + The default cookie is for URL/sony. For some commands we need it for + the root path. Only for api v4 """ - if self.pin == None: + if self.pin is None: return # todo fix cookies cookies = None - #cookies = requests.cookies.RequestsCookieJar() - #cookies.set("auth", self.cookies.get("auth")) + # cookies = requests.cookies.RequestsCookieJar() + # cookies.set("auth", self.cookies.get("auth")) username = '' - base64string = base64.encodebytes(('%s:%s' % (username, self.pin)).encode()) \ - .decode().replace('\n', '') + base64string = base64.encodebytes(('%s:%s' % (username, self.pin)) + .encode()).decode().replace('\n', '') registration_action = self.get_action("register") @@ -317,7 +312,9 @@ def register(self): # protocoll version 1 and 2 if registration_action.mode < 3: registration_response = self.send_http( - registration_action.url, method=HttpMethod.GET, raise_errors=True) + registration_action.url, + method=HttpMethod.GET, + raise_errors=True) if registration_response.text == "": registration_result = AuthenticationResult.SUCCESS else: @@ -347,8 +344,10 @@ def register(self): ).encode('utf-8') try: - response = self.send_http(registration_action.url, method=HttpMethod.POST, - data=authorization, raise_errors=True) + response = self.send_http(registration_action.url, + method=HttpMethod.POST, + data=authorization, + raise_errors=True) except requests.exceptions.HTTPError as ex: _LOGGER.error("[W] HTTPError: " + str(ex)) registration_result = AuthenticationResult.PIN_NEEDED @@ -364,7 +363,8 @@ def register(self): else: raise ValueError( - "Registration mode {0} is not supported".format(registration_action.mode)) + "Registration mode {0} is not supported" + .format(registration_action.mode)) return registration_result @@ -383,8 +383,10 @@ def send_authentication(self, pin): if registration_action.mode == 3: try: self.send_http( - self.get_action("register").url, method=HttpMethod.GET, raise_errors=True) - except: + self.get_action("register").url, + method=HttpMethod.GET, + raise_errors=True) + except Exception: return False else: self.pin = pin @@ -416,7 +418,7 @@ def send_authentication(self, pin): method=HttpMethod.post, data=authorization, raise_errors=True) - except: + except Exception: return False else: resp = response.json() @@ -431,7 +433,8 @@ def send_authentication(self, pin): return True - def send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): + def send_http(self, url, method, data=None, headers=None, log_errors=True, + raise_errors=False): """ Send request command via HTTP json to Sony Bravia.""" if headers is None: @@ -486,11 +489,12 @@ def post_soap_request(self, url, params, action): params +\ "" +\ "" - response = self.send_http(url, method=HttpMethod.POST,headers=headers, data=data) + response = self.send_http(url, method=HttpMethod.POST, + headers=headers, data=data) if response is not None: return response.content.decode("utf-8") - def send_req_ircc(self, params, log_errors=True): + def send_req_ircc(self, params): """Send an IRCC command via HTTP to Sony Bravia.""" data = "" +\ @@ -523,7 +527,7 @@ def get_playing_status(self): def get_power_status(self): url = self.actionlist_url try: - response = self.send_http(url, HttpMethod.GET, + self.send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) except Exception as ex: _LOGGER.debug(ex) @@ -533,14 +537,13 @@ def get_power_status(self): # def get_source(self, source): # pass - def start_app(self, app_name, log_errors=True): + def start_app(self, app_name): """Start an app by name""" # sometimes device does not start app if already running one self.home() url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) data = "LOCATION: {0}/run".format(url) self.send_http(url, HttpMethod.POST, data=data) - pass def send_command(self, name): if len(self.commands) == 0: @@ -555,24 +558,22 @@ def send_command(self, name): raise ValueError('Failed to read command list from device.') def get_action(self, name): - if not name in self.actions and len(self.actions) == 0: + if name not in self.actions and not self.actions: self.update_service_urls() - if not name in self.actions and len(self.actions) == 0: + if name not in self.actions and not self.actions: raise ValueError('Failed to read action list from device.') return self.actions[name] def power(self, on): - if (on): + if on: self.wakeonlan() + # Try using the power on command incase the WOL doesn't work if not self.get_power_status(): self.send_command('Power') else: self.send_command('Power') - # Try using the power on command incase the WOL doesn't work - - def get_apps(self): return list(self.apps.keys()) From 8a7e81aaf85acb3ca32e738d9ec475062b1ef5b4 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Wed, 3 Apr 2019 13:36:21 +0200 Subject: [PATCH 054/170] Added blank line --- sonyapilib/device.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index ae6f2d8..8be67a9 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -49,6 +49,7 @@ def __init__(self, xml_data): else: setattr(self, arg, xml_data[arg]) + class SonyDevice(): """Contains all data for the device.""" From d54956abe0bb7edeac88c45a7a94ac2caf430225 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Wed, 3 Apr 2019 15:02:54 +0200 Subject: [PATCH 055/170] More lint errors --- sonyapilib/device.py | 42 ++++++++++++++++++------------------------ 1 file changed, 18 insertions(+), 24 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 8be67a9..af1d275 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -1,4 +1,4 @@ -"""Sony Mediaplayer lib""" +"""Sony Mediaplayer lib.""" import logging import base64 import json @@ -28,7 +28,7 @@ class HttpMethod(Enum): class XmlApiObject(): - """ Holds data for a device action or a command """ + """Holds data for a device action or a command.""" def __init__(self, xml_data): self.name = None @@ -55,7 +55,7 @@ class SonyDevice(): def __init__(self, host, nickname, port=50001, dmr_port=52323, app_port=50202, ircc_location=None): - """ Init the device with the entry point""" + """Init the device with the entry point.""" self.host = host self.nickname = nickname self.ircc_url = ircc_location @@ -96,9 +96,7 @@ def init_device(self): @staticmethod def discover(): - """ - Discover all available devices. - """ + """Discover all available devices.""" discovery = ssdp.SSDPDiscovery() devices = [] for device in discovery.discover("urn:schemas-sony-com:service:headersIRCC:1"): @@ -131,8 +129,7 @@ def wakeonlan(self): wakeonlan.send_magic_packet(self.mac, ip_address=self.host) def update_service_urls(self): - """Initalize the device by reading the necessary resources from it.""" - + """Init device by reading the necessary resources.""" lirc_url = urllib.parse.urlparse(self.ircc_url) response = self.send_http(self.ircc_url, method=HttpMethod.GET) if response is None: @@ -213,8 +210,8 @@ def update_service_urls(self): raw_data = response.text xml_data = xml.etree.ElementTree.fromstring(raw_data) for device in xml_data.findall("{0}device".format(urn_upnp_device)): - serviceList = device.find("{0}serviceList".format(urn_upnp_device)) - for service in serviceList: + service_list = device.find("{0}serviceList".format(urn_upnp_device)) + for service in service_list: service_id = service.find( "{0}serviceId".format(urn_upnp_device)) if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: @@ -270,13 +267,12 @@ def update_applist(self): def recreate_authentication(self): """ - The default cookie is for URL/sony. For some commands we need it for - the root path. + Default cookie is for URL/sony. + For some commands we need it for the root path. Only for api v4 """ - if self.pin is None: - return + return False # todo fix cookies cookies = None @@ -433,16 +429,14 @@ def send_authentication(self, pin): self.init_device() return True - def send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): - """ Send request command via HTTP json to Sony Bravia.""" - + """Send request command via HTTP json to Sony Bravia.""" if headers is None: headers = self.headers if url is None: - return + return False _LOGGER.debug("Calling http url {0} method {1}".format(url, str(method))) @@ -492,12 +486,12 @@ def post_soap_request(self, url, params, action): "" response = self.send_http(url, method=HttpMethod.POST, headers=headers, data=data) - if response is not None: + if response: return response.content.decode("utf-8") + return False def send_req_ircc(self, params): """Send an IRCC command via HTTP to Sony Bravia.""" - data = "" +\ "" + params + "" +\ "" @@ -539,7 +533,7 @@ def get_power_status(self): # pass def start_app(self, app_name): - """Start an app by name""" + """Start an app by name.""" # sometimes device does not start app if already running one self.home() url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) @@ -547,14 +541,14 @@ def start_app(self, app_name): self.send_http(url, HttpMethod.POST, data=data) def send_command(self, name): - if len(self.commands) == 0: + if not self.commands: self.update_commands() - if len(self.commands) > 0: + if not self.commands: if name in self.commands: self.send_req_ircc(self.commands[name].value) else: - raise ValueError('Unknown command: %s', name) + raise ValueError('Unknown command: %s' % name) else: raise ValueError('Failed to read command list from device.') From 7f001c2571d2836a675a02f8947eb0bd1a500ddb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Wed, 3 Apr 2019 20:50:55 +0200 Subject: [PATCH 056/170] Do some codestyling in setup.py --- setup.py | 43 +++++++++++++++++++++++-------------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/setup.py b/setup.py index 4453bb6..24afa00 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,9 @@ from __future__ import absolute_import import sys import os -from setuptools import setup, find_packages -# import subprocess +from setuptools import setup + + sys.path.insert(0, '.') CURRENT_DIR = os.path.dirname(__file__) @@ -12,22 +13,24 @@ # to deploy to pip, please use # make pythonpack # python setup.py register sdist upload -# and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" -setup(name='sonyapilib', - packages = ['sonyapilib'], # this must be the same as the name above - version = '0.3.11', - description = 'Lib to control sony devices with their soap api', - author = 'Alexander Mohr', - author_email = 'sonyapilib@mohr.io', - url = 'https://github.com/alexmohr/sonyapilib', # use the URL to the github repo - download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3.11', - keywords = ['soap', 'sony', 'api'], # arbitrary keywords - classifiers = [], - install_requires=[ - 'jsonpickle', - 'setuptools', - 'requests', - 'wakeonlan' - ], - +# and be sure to test it firstly using +# "python setup.py register sdist upload -r pypitest" +setup( + name='sonyapilib', + packages=['sonyapilib'], # this must be the same as the name above + version='0.3.11', + description='Lib to control sony devices with their soap api', + author='Alexander Mohr', + author_email='sonyapilib@mohr.io', + # use the URL to the github repo + url='https://github.com/alexmohr/sonyapilib', + download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3.11', + keywords=['soap', 'sony', 'api'], # arbitrary keywords + classifiers=[], + install_requires=[ + 'jsonpickle', + 'setuptools', + 'requests', + 'wakeonlan', + ], ) From 30617cb779f60dafc4cad37191ba44313f7f561f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Wed, 3 Apr 2019 20:55:25 +0200 Subject: [PATCH 057/170] Sort imports in device.py --- sonyapilib/device.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index ec544a4..0a7afab 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -1,22 +1,23 @@ """ Sony Mediaplayer lib """ -import logging +from enum import Enum import base64 import collections import json +import logging import socket import struct -import requests import urllib.parse import xml.etree.ElementTree -import requests -from enum import Enum -import wakeonlan import jsonpickle +import requests +import wakeonlan from sonyapilib import ssdp + + _LOGGER = logging.getLogger(__name__) TIMEOUT = 5 @@ -238,7 +239,7 @@ def _parse_dmr(self, data): action = XmlApiObject(None) action.url = base_url + "/system" self.actions["getRemoteCommandList"] = action - + def _update_commands(self): From 4f1e63ce6fad95888ea9b675d5011deafd562c6e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:06:01 +0200 Subject: [PATCH 058/170] Refactoring, cleanup, added tests, continuued v4 impl. --- .coveralls.yaml | 2 + .travis.yml | 3 +- README.md | 2 + setup.py | 6 +- sonyapilib/device.py | 556 +++++++++++++++++------------ sonyapilib/ssdp.py | 23 +- tests/deviceTest.py | 110 ++++-- tests/simple_example.py | 41 +++ tests/xml/actionlist.xml | 13 + tests/{ => xml}/dmr_v3.xml | 0 tests/{ => xml}/dmr_v4.xml | 0 tests/xml/getRemoteCommandList.xml | 51 +++ tests/xml/getSysteminformation.xml | 39 ++ tests/{ => xml}/ircc.xml | 0 14 files changed, 575 insertions(+), 271 deletions(-) create mode 100644 .coveralls.yaml create mode 100644 tests/simple_example.py create mode 100644 tests/xml/actionlist.xml rename tests/{ => xml}/dmr_v3.xml (100%) rename tests/{ => xml}/dmr_v4.xml (100%) create mode 100644 tests/xml/getRemoteCommandList.xml create mode 100644 tests/xml/getSysteminformation.xml rename tests/{ => xml}/ircc.xml (100%) diff --git a/.coveralls.yaml b/.coveralls.yaml new file mode 100644 index 0000000..61eaf36 --- /dev/null +++ b/.coveralls.yaml @@ -0,0 +1,2 @@ +service_name: travis-pro +repo_token: 0LnUggY7qWBesuadHn8esHdbtDi45kAfi \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 1642543..f080143 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,4 +5,5 @@ cache: pip install: - pip install . script: - - python tests/deviceTest.py \ No newline at end of file + - python tests/deviceTest.py + - pylint sonyapilib/*. \ No newline at end of file diff --git a/README.md b/README.md index 2da4fb0..eb69092 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ # Sonyapilib +[![Build Status](https://travis-ci.org/alexmohr/sonyapilib.svg?branch=master)](https://travis-ci.org/alexmohr/sonyapilib) +[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=master)](https://coveralls.io/github/alexmohr/sonyapilib?branch=master) Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant wich you can find here: https://github.com/dilruacs/media_player.sony diff --git a/setup.py b/setup.py index 4453bb6..52a3e16 100644 --- a/setup.py +++ b/setup.py @@ -15,18 +15,18 @@ # and be sure to test it firstly using "python setup.py register sdist upload -r pypitest" setup(name='sonyapilib', packages = ['sonyapilib'], # this must be the same as the name above - version = '0.3.11', + version = '0.4.0', description = 'Lib to control sony devices with their soap api', author = 'Alexander Mohr', author_email = 'sonyapilib@mohr.io', url = 'https://github.com/alexmohr/sonyapilib', # use the URL to the github repo - download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.3.11', + download_url = 'https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.0', keywords = ['soap', 'sony', 'api'], # arbitrary keywords classifiers = [], install_requires=[ 'jsonpickle', 'setuptools', - 'requests', + 'requests', 'wakeonlan' ], diff --git a/sonyapilib/device.py b/sonyapilib/device.py index ad895f4..65b9fda 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -1,4 +1,5 @@ """Sony Mediaplayer lib.""" +import uuid import logging import base64 import json @@ -7,6 +8,7 @@ from enum import Enum import requests + import wakeonlan import jsonpickle @@ -20,17 +22,20 @@ class AuthenticationResult(Enum): + """Stores the result of the authentication process.""" SUCCESS = 0 ERROR = 1 PIN_NEEDED = 2 class HttpMethod(Enum): + """Defines which http method is used.""" GET = 0 POST = 1 class XmlApiObject(): + # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" def __init__(self, xml_data): @@ -40,6 +45,7 @@ def __init__(self, xml_data): self.type = None self.value = None self.mac = None + # pylint: disable=invalid-name self.id = None if xml_data is not None: @@ -54,6 +60,8 @@ def __init__(self, xml_data): class SonyDevice(): + # pylint: disable=too-many-public-methods + # pylint: disable=too-many-instance-attributes """Contains all data for the device.""" def __init__(self, host, nickname): @@ -79,19 +87,22 @@ def __init__(self, host, nickname): self.cookies = None self.mac = None self.is_v4 = False + self.uuid = uuid.uuid4() self.ircc_url = "http://{0}:{1}/Ircc.xml".format(host, self.ircc_port) - self.irccscpd_url = "http://{0}:{1}/IRCCSCPD.xml".format(host, self.ircc_port) - self.dmr_url = "http://{0}:{1}/dmr.xml".format(self.host, self.dmr_port) + self.irccscpd_url = "http://{0}:{1}/IRCCSCPD.xml".format( + host, self.ircc_port) + self.dmr_url = "http://{0}:{1}/dmr.xml".format( + self.host, self.dmr_port) self.app_url = "http://{0}:{1}".format(self.host, self.app_port) - if not self.actions and self.pin is not None: - self.init_device() - - def init_device(self): + def _init_device(self): self._update_service_urls() - self._update_commands() - self._update_applist() + + if self.pin: + self._recreate_authentication() + self._update_commands() + self._update_applist() @staticmethod def discover(): @@ -112,32 +123,59 @@ def load_from_json(data): return jsonpickle.decode(data) def save_to_json(self): - """ Save this device configuration into a json """ + """Save this device configuration into a json.""" return jsonpickle.dumps(self) def _update_service_urls(self): - """ Initialize the device by reading the necessary resources from it """ + """Initialize the device by reading the necessary resources from it.""" response = self._send_http(self.dmr_url, method=HttpMethod.GET) if not response: _LOGGER.error("Failed to get DMR") return self._parse_dmr(response.text) - self._recreate_authentication() - if not self.is_v4: - response = self._send_http(self.ircc_url, method=HttpMethod.GET) - self._parse_ircc(response.text) + try: + if self.is_v4: + pass + else: + response = self._send_http( + self.ircc_url, method=HttpMethod.GET) + if response: + self._parse_ircc(response.text) - if len(self.commands) > 0: - self._update_commands() - self._update_applist() + response = self._send_http( + self.actionlist_url, method=HttpMethod.GET) + if response: + self._parse_action_list(response.text) - def _parse_ircc(self, data): - response = self._send_http(self.ircc_url, method=HttpMethod.GET) - if not response: - return + response = self._send_http( + self._get_action("getSystemInformation").url, method=HttpMethod.GET) + if response: + self._parse_system_information(response.text) + + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error("failed to get device information: %s", str(ex)) + + def _parse_action_list(self, data): + xml_data = xml.etree.ElementTree.fromstring(data) + for element in xml_data.findall("action"): + action = XmlApiObject(element.attrib) + self.actions[action.name] = action + + if action.name == "register": + # the authentication later on is based on the device id and the mac + # todo maybe refactor this to requests + # http://docs.python-requests.org/en/master/_modules/requests/api/?highlight=param + action.url = "{0}?name={1}®istrationType=initial&deviceId={2}".format( + action.url, + urllib.parse.quote(self.nickname), + urllib.parse.quote(self.get_device_id())) + + if action.mode == 3: + action.url = action.url + "&wolSupport=true" + def _parse_ircc(self, data): xml_data = xml.etree.ElementTree.fromstring(data) # the action list contains everything the device supports @@ -150,32 +188,13 @@ def _parse_ircc(self, data): .find("{0}serviceList".format(URN_UPNP_DEVICE))\ .findall("{0}service".format(URN_UPNP_DEVICE)) - # read action list - response = self._send_http(self.actionlist_url, method=HttpMethod.GET) - if not response: - raw_data = response.text - xml_data = xml.etree.ElementTree.fromstring(raw_data) - for element in xml_data.findall("action"): - action = XmlApiObject(element.attrib) - self.actions[action.name] = action - - # some data has to overwritten for the registration to work properly - if action.name == "register": - if action.mode < 4: - # the authentication later on is based on the device id and the mac - # todo maybe refactor this to requests http://docs.python-requests.org/en/master/_modules/requests/api/?highlight=param - action.url = "{0}?name={1}®istrationType=initial&deviceId={1}".format( - action.url, urllib.parse.quote(self.nickname)) - if action.mode == 3: - action.url = action.url + "&wolSupport=true" - lirc_url = urllib.parse.urlparse(self.ircc_url) if services: # read service list for service in services: service_id = service.find( "{0}serviceId".format(URN_UPNP_DEVICE)) - if not service_id: + if service_id is None: continue if "urn:schemas-sony-com:serviceId:IRCC" not in service_id.text: continue @@ -185,26 +204,21 @@ def _parse_ircc(self, data): service_url = lirc_url.scheme + "://" + lirc_url.netloc self.control_url = service_url + service_location - # get systeminformation - response = self._send_http( - self.get_action("getSystemInformation").url, method=HttpMethod.GET) - - if response is not None: - raw_data = response.text - xml_data = xml.etree.ElementTree.fromstring(raw_data) - for element in xml_data.findall("supportFunction"): - for function in element.findall("function"): - if function.attrib["name"] == "WOL": - self.mac = function.find( - "functionItem").attrib["value"] + def _parse_system_information(self, data): + xml_data = xml.etree.ElementTree.fromstring(data) + for element in xml_data.findall("supportFunction"): + for function in element.findall("function"): + if function.attrib["name"] == "WOL": + self.mac = function.find( + "functionItem").attrib["value"] def _parse_dmr(self, data): lirc_url = urllib.parse.urlparse(self.ircc_url) xml_data = xml.etree.ElementTree.fromstring(data) for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): - serviceList = device.find( + service_list = device.find( "{0}serviceList".format(URN_UPNP_DEVICE)) - for service in serviceList: + for service in service_list: service_id = service.find( "{0}serviceId".format(URN_UPNP_DEVICE)) if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: @@ -212,19 +226,21 @@ def _parse_dmr(self, data): transport_location = service.find( "{0}controlURL".format(URN_UPNP_DEVICE)).text self.av_transport_url = "{0}://{1}:{2}{3}".format( - lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + lirc_url.scheme, lirc_url.netloc.split(":")[0], + self.dmr_port, transport_location) - # this is only for v4 devices. + # this is only true for v4 devices. if not "av:X_ScalarWebAPI_ServiceType" in data: return self.is_v4 = True - deviceInfo = "{0}X_ScalarWebAPI_DeviceInfo"\ + device_info_name = "{0}X_ScalarWebAPI_DeviceInfo"\ .format(URN_SCALAR_WEB_API_DEVICE_INFO) for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): - for deviceInfo in device.findall(deviceInfo): - base_url = deviceInfo.find("{0}X_ScalarWebAPI_BaseURL"\ + for device_info in device.findall(device_info_name): + base_url = device_info.find( + "{0}X_ScalarWebAPI_BaseURL" .format(URN_SCALAR_WEB_API_DEVICE_INFO))\ .text @@ -235,62 +251,64 @@ def _parse_dmr(self, data): action = XmlApiObject(None) action.url = base_url + "/system" + action.value = "getRemoteControllerInfo" self.actions["getRemoteCommandList"] = action - def _update_commands(self): + """Update the list of commands.""" - # needs to be registered to do that + # need to be registered to do that if not self.pin: + _LOGGER.info("Registration necessary to read command list.") return - url = self.get_action("getRemoteCommandList").url - if self.get_action("register").mode < 4: + url = self._get_action("getRemoteCommandList").url + if self._get_action("register").mode < 4: response = self._send_http(url, method=HttpMethod.GET) if response: - xml_data = xml.etree.ElementTree.fromstring(response.text) - - for command in xml_data.findall("command"): - name = command.get("name") - self.commands[name] = XmlApiObject(command.attrib) + self._parse_command_list(response.text) + else: + _LOGGER.error("Failed to get response") else: - response = self._send_http( - url, method=HttpMethod.POST, data=self._create_api_json("getRemoteControllerInfo", 1)) - if response: - json = response.json() - # todo parse json - if not json.get('error'): - # todo this does not fit 100% with the structure of this lib. - # see github issue#2 - self.commands = json.get('result')[1] - else: - _LOGGER.error("JSON request error: " + - json.dumps(json, indent=4)) + action_name = "getRemoteCommandList" + action = self.actions[action_name] + json_data = self._create_api_json(action.value) + + resp = self._request_json(action.url, json_data, None) + if resp and not resp.get('error'): + # todo parse this into the old structure. + self.commands = resp.get('result')[1] + else: + _LOGGER.error("JSON request error: %s", + json.dumps(resp, indent=4)) + + def _parse_command_list(self, data): + xml_data = xml.etree.ElementTree.fromstring(data) + for command in xml_data.findall("command"): + name = command.get("name") + self.commands[name] = XmlApiObject(command.attrib) def _update_applist(self): + """Update the list of apps which are supported by the device.""" url = self.app_url + "/appslist" response = self._send_http(url, method=HttpMethod.GET) + # todo add support for v4 if response: xml_data = xml.etree.ElementTree.fromstring(response.text) apps = xml_data.findall(".//app") for app in apps: name = app.find("name").text - id = app.find("id").text + app_id = app.find("id").text data = XmlApiObject(None) data.name = name - data.id = id + data.id = app_id self.apps[name] = data def _recreate_authentication(self): - """ - The default cookie is for URL/sony. For some commands we need it for the root path. - """ - - if not self.pin: - return + """The default cookie is for URL/sony. For some commands we need it for the root path.""" # todo fix cookies - cookies = None + # cookies = None # cookies = requests.cookies.RequestsCookieJar() # cookies.set("auth", self.cookies.get("auth")) @@ -298,7 +316,7 @@ def _recreate_authentication(self): base64string = base64.encodebytes(('%s:%s' % (username, self.pin)) .encode()).decode().replace('\n', '') - registration_action = self.get_action("register") + registration_action = self._get_action("register") self.headers['Authorization'] = "Basic %s" % base64string if registration_action.mode == 3: @@ -306,52 +324,68 @@ def _recreate_authentication(self): elif registration_action.mode == 4: self.headers['Connection'] = "keep-alive" - return cookies - def _request_json(self, url, params, log_errors=True): """Send request command via HTTP json to Sony Bravia.""" + + headers = {} + built_url = 'http://{}/{}'.format(self.host, url) - response = self._send_http(url, HttpMethod.POST, params) - html = json.loads(response.content.decode('utf-8')) - return html + try: + response = requests.post(built_url, + data=params.encode("UTF-8"), + cookies=self.cookies, + timeout=TIMEOUT, + headers=headers) + + except requests.exceptions.HTTPError as exception_instance: + if log_errors: + _LOGGER.error("HTTPError: %s", str(exception_instance)) + + except Exception as exception_instance: # pylint: disable=broad-except + if log_errors: + _LOGGER.error("Exception: %s", str(exception_instance)) - def _create_api_json(self, method, id, params=None): + else: + html = json.loads(response.content.decode('utf-8')) + return html + + def _create_api_json(self, method, params=None): + # pylint: disable=invalid-name """Create json data which will be send via post for the V4 api""" if not params: params = [{ "clientid": self.get_device_id(), "nickname": self.nickname - }, - [{ - "clientid": self.get_device_id(), - "nickname": self.nickname, - "value": "yes", - "function": "WOL" - }] - ] + }, [{ + "clientid": self.get_device_id(), + "nickname": self.nickname, + "value": "yes", + "function": "WOL" + }]] ret = json.dumps( { "method": method, "params": params, - "id": id, + "id": 1, "version": "1.0" }) return ret def _send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): + # pylint: disable=too-many-arguments """Send request command via HTTP json to Sony Bravia.""" if not headers: headers = self.headers if not url: - return + return None _LOGGER.debug( - "Calling http url {0} method {1}".format(url, str(method))) + "Calling http url %s method %s", url, method) try: params = "" @@ -374,12 +408,12 @@ def _send_http(self, url, method, data=None, headers=None, log_errors=True, rais response.raise_for_status() except requests.exceptions.HTTPError as ex: if log_errors: - _LOGGER.error("HTTPError: " + str(ex)) + _LOGGER.error("HTTPError: %s", str(ex)) if raise_errors: raise except Exception as ex: # pylint: disable=broad-except if log_errors: - _LOGGER.error("Exception: " + str(ex)) + _LOGGER.error("Exception: %s", str(ex)) if raise_errors: raise else: @@ -391,12 +425,13 @@ def _post_soap_request(self, url, params, action): "Content-Type": "text/xml" } - data = "" +\ - "" +\ - params +\ - "" +\ - "" + data = """ + + + {0} + + """.format(params) response = self._send_http( url, method=HttpMethod.POST, headers=headers, data=data) if response: @@ -405,9 +440,10 @@ def _post_soap_request(self, url, params, action): def _send_req_ircc(self, params): """Send an IRCC command via HTTP to Sony Bravia.""" - data = "" +\ - "" + params + "" +\ - "" + + data = """ + {0} + """.format(params) action = "urn:schemas-sony-com:service:IRCC:1#X_SendIRCC" content = self._post_soap_request( @@ -415,23 +451,32 @@ def _send_req_ircc(self, params): return content def get_device_id(self): - return "TVSideView:{0}".format(self.mac) + """Returns the id which is used for the registration.""" + return "TVSideView:{0}".format(self.uuid) def register(self): + # pylint: disable=too-many-branches """ - Register at the api. The name which will be displayed in the UI of the device. Make sure this name does not exist yet + Register at the api. The name which will be displayed in the UI of the device. + Make sure this name does not exist yet For this the device must be put in registration mode. - The tested sd5500 has no separte mode but allows registration in the overview " + The tested sd5500 has no separate mode but allows registration in the overview " """ registration_result = AuthenticationResult.ERROR - registration_action = registration_action = self.get_action("register") + registration_action = registration_action = self._get_action( + "register") # protocol version 1 and 2 if registration_action.mode < 3: - registration_response = self._send_http( - registration_action.url, method=HttpMethod.GET, raise_errors=True) - registration_result = AuthenticationResult.SUCCESS + try: + self._send_http( + registration_action.url, + method=HttpMethod.GET, + raise_errors=True) + registration_result = AuthenticationResult.SUCCESS + except requests.exceptions.HTTPError: + registration_result = AuthenticationResult.ERROR # protocol version 3 elif registration_action.mode == 3: @@ -439,9 +484,10 @@ def register(self): self._send_http(registration_action.url, method=HttpMethod.GET, raise_errors=True) except requests.exceptions.HTTPError as ex: - _LOGGER.error("[W] HTTPError: " + str(ex)) - # todo set the correct result. - registration_result = AuthenticationResult.PIN_NEEDED + if ex.response.status_code == 401: + registration_result = AuthenticationResult.PIN_NEEDED + else: + registration_result = AuthenticationResult.ERROR # newest protocol version 4 this is the same method as braviarc uses elif registration_action.mode == 4: @@ -451,18 +497,19 @@ def register(self): headers = { "Content-Type": "application/json" } - response = self._send_http(registration_action.url, method=HttpMethod.POST, headers=headers, + response = self._send_http(registration_action.url, + method=HttpMethod.POST, headers=headers, data=authorization, raise_errors=True) + except requests.exceptions.HTTPError as ex: - _LOGGER.error("[W] HTTPError: " + str(ex)) + _LOGGER.error("[W] HTTPError: %s", str(ex)) # todo set the correct result. registration_result = AuthenticationResult.PIN_NEEDED except Exception as ex: # pylint: disable=broad-except - _LOGGER.error("[W] Exception: " + str(ex)) + _LOGGER.error("[W] Exception: %s", str(ex)) else: resp = response.json() - _LOGGER.debug(json.dumps(resp, indent=4)) if not resp or not resp.get('error'): self.cookies = response.cookies registration_result = AuthenticationResult.SUCCESS @@ -470,14 +517,15 @@ def register(self): else: raise ValueError( "Regisration mode {0} is not supported".format(registration_action.mode)) - + if AuthenticationResult.SUCCESS: - self.init_device() + self._init_device() return registration_result def send_authentication(self, pin): - registration_action = self.get_action("register") + """Authenticate against the device.""" + registration_action = self._get_action("register") # they do not need a pin if registration_action.mode < 3: @@ -485,16 +533,24 @@ def send_authentication(self, pin): self.pin = pin self._recreate_authentication() - self.register() + result = self.register() + + if AuthenticationResult.SUCCESS == result: + self._init_device() + return True + + return False def wakeonlan(self): + """Starts the device either via wakeonlan.""" if self.mac: wakeonlan.send_magic_packet(self.mac, ip_address=self.host) def get_playing_status(self): - data = '' + \ - '0' + \ - '' + """Get the status of playback from the device""" + data = """ + 0 + """ action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" @@ -507,20 +563,22 @@ def get_playing_status(self): return state def get_power_status(self): + """Checks if the device is online.""" url = self.actionlist_url try: - response = self._send_http(url, HttpMethod.GET, - log_errors=False, raise_errors=True) - except Exception as ex: + # todo parse response + self._send_http(url, HttpMethod.GET, + log_errors=False, raise_errors=True) + except requests.exceptions.HTTPError as ex: _LOGGER.debug(ex) return False return True - def send_command(self, name): - if len(self.commands) == 0: - self._update_commands() - + def _send_command(self, name): if not self.commands: + self._init_device() + + if self.commands: if name in self.commands: self._send_req_ircc(self.commands[name].value) else: @@ -528,195 +586,227 @@ def send_command(self, name): else: raise ValueError('Failed to read command list from device.') - def get_action(self, name): - if not name in self.actions and len(self.actions) == 0: - self._update_service_urls() - if not name in self.actions and len(self.actions) == 0: + def _get_action(self, name): + """Get the action object for the action with the given name""" + if name not in self.actions and not self.actions: + if name not in self.actions and not self.actions: raise ValueError('Failed to read action list from device.') return self.actions[name] - def start_app(self, app_name, log_errors=True): + def start_app(self, app_name): """Start an app by name""" # sometimes device does not start app if already running one + # todo add support for v4 self.home() url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) data = "LOCATION: {0}/run".format(url) self._send_http(url, HttpMethod.POST, data=data) - def power(self, on): - if on: + def power(self, power_on): + """Powers the device on or shuts it off.""" + if power_on: self.wakeonlan() # Try using the power on command incase the WOL doesn't work if not self.get_power_status(): # Try using the power on command incase the WOL doesn't work - self.send_command('Power') + self._send_command('Power') else: - self.send_command('Power') + self._send_command('Power') def get_apps(self): + """Get the apps from the stored dict.""" return list(self.apps.keys()) def up(self): - self.send_command('Up') + # pylint: disable=invalid-name + """Sends the command 'up' to the connected device.""" + self._send_command('Up') def confirm(self): - self.send_command('Confirm') + """Sends the command 'confirm' to the connected device.""" + self._send_command('Confirm') def down(self): - self.send_command('Down') + """Sends the command 'down' to the connected device.""" + self._send_command('Down') def right(self): - self.send_command('Right') + """Sends the command 'right' to the connected device.""" + self._send_command('Right') def left(self): - self.send_command('Left') + """Sends the command 'left' to the connected device.""" + self._send_command('Left') def home(self): - self.send_command('Home') + """Sends the command 'home' to the connected device.""" + self._send_command('Home') def options(self): - self.send_command('Options') + """Sends the command 'options' to the connected device.""" + self._send_command('Options') def returns(self): - self.send_command('Return') + """Sends the command 'returns' to the connected device.""" + self._send_command('Return') def num1(self): - self.send_command('Num1') + """Sends the command 'num1' to the connected device.""" + self._send_command('Num1') def num2(self): - self.send_command('Num2') + """Sends the command 'num2' to the connected device.""" + self._send_command('Num2') def num3(self): - self.send_command('Num3') + """Sends the command 'num3' to the connected device.""" + self._send_command('Num3') def num4(self): - self.send_command('Num4') + """Sends the command 'num4' to the connected device.""" + self._send_command('Num4') def num5(self): - self.send_command('Num5') + """Sends the command 'num5' to the connected device.""" + self._send_command('Num5') def num6(self): - self.send_command('Num6') + """Sends the command 'num6' to the connected device.""" + self._send_command('Num6') def num7(self): - self.send_command('Num7') + """Sends the command 'num7' to the connected device.""" + self._send_command('Num7') def num8(self): - self.send_command('Num8') + """Sends the command 'num8' to the connected device.""" + self._send_command('Num8') def num9(self): - self.send_command('Num9') + """Sends the command 'num9' to the connected device.""" + self._send_command('Num9') def num0(self): - self.send_command('Num0') + """Sends the command 'num0' to the connected device.""" + self._send_command('Num0') def display(self): - self.send_command('Display') + """Sends the command 'display' to the connected device.""" + self._send_command('Display') def audio(self): - self.send_command('Audio') + """Sends the command 'audio' to the connected device.""" + self._send_command('Audio') - def subTitle(self): - self.send_command('SubTitle') + def sub_title(self): + """Sends the command 'subTitle' to the connected device.""" + self._send_command('SubTitle') def favorites(self): - self.send_command('Favorites') + """Sends the command 'favorites' to the connected device.""" + self._send_command('Favorites') def yellow(self): - self.send_command('Yellow') + """Sends the command 'yellow' to the connected device.""" + self._send_command('Yellow') def blue(self): - self.send_command('Blue') + """Sends the command 'blue' to the connected device.""" + self._send_command('Blue') def red(self): - self.send_command('Red') + """Sends the command 'red' to the connected device.""" + self._send_command('Red') def green(self): - self.send_command('Green') + """Sends the command 'green' to the connected device.""" + self._send_command('Green') def play(self): - self.send_command('Play') + """Sends the command 'play' to the connected device.""" + self._send_command('Play') def stop(self): - self.send_command('Stop') + """Sends the command 'stop' to the connected device.""" + self._send_command('Stop') def pause(self): - self.send_command('Pause') + """Sends the command 'pause' to the connected device.""" + self._send_command('Pause') def rewind(self): - self.send_command('Rewind') + """Sends the command 'rewind' to the connected device.""" + self._send_command('Rewind') def forward(self): - self.send_command('Forward') + """Sends the command 'forward' to the connected device.""" + self._send_command('Forward') def prev(self): - self.send_command('Prev') + """Sends the command 'prev' to the connected device.""" + self._send_command('Prev') def next(self): - self.send_command('Next') + """Sends the command 'next' to the connected device.""" + self._send_command('Next') def replay(self): - self.send_command('Replay') + """Sends the command 'replay' to the connected device.""" + self._send_command('Replay') def advance(self): - self.send_command('Advance') + """Sends the command 'advance' to the connected device.""" + self._send_command('Advance') def angle(self): - self.send_command('Angle') + """Sends the command 'angle' to the connected device.""" + self._send_command('Angle') - def topMenu(self): - self.send_command('TopMenu') + def top_menu(self): + """Sends the command 'top_menu' to the connected device.""" + self._send_command('TopMenu') - def popUpMenu(self): - self.send_command('PopUpMenu') + def pop_up_menu(self): + """Sends the command 'pop_up_menu' to the connected device.""" + self._send_command('PopUpMenu') def eject(self): - self.send_command('Eject') + """Sends the command 'eject' to the connected device.""" + self._send_command('Eject') def karaoke(self): - self.send_command('Karaoke') + """Sends the command 'karaoke' to the connected device.""" + self._send_command('Karaoke') def netflix(self): - self.send_command('Netflix') + """Sends the command 'netflix' to the connected device.""" + self._send_command('Netflix') - def mode3D(self): - self.send_command('Mode3D') + def mode_3d(self): + """Sends the command 'mode_3d' to the connected device.""" + self._send_command('Mode3D') - def zoomIn(self): - self.send_command('ZoomIn') + def zoom_in(self): + """Sends the command 'zoom_in' to the connected device.""" + self._send_command('ZoomIn') - def zoomOut(self): - self.send_command('ZoomOut') + def zoom_out(self): + """Sends the command 'zoom_out' to the connected device.""" + self._send_command('ZoomOut') - def browserBack(self): - self.send_command('BrowserBack') + def browser_back(self): + """Sends the command 'browser_back' to the connected device.""" + self._send_command('BrowserBack') - def browserForward(self): - self.send_command('BrowserForward') + def browser_forward(self): + """Sends the command 'browser_forward' to the connected device.""" + self._send_command('BrowserForward') - def browserBookmarkList(self): - self.send_command('BrowserBookmarkList') + def browser_bookmark_list(self): + """Sends the command 'browser_bookmarkList' to the connected device.""" + self._send_command('BrowserBookmarkList') def list(self): - self.send_command('List') - - -if __name__ == "__main__": - - stored_config = "bluray.json" - device = None - # device must be on for registration - host = "192.168.178.23" - device = SonyDevice(host, "SonyApiLib Python Test10") - device.register() - pin = input("Enter the PIN displayed at your device: ") - device.send_authentication(pin) - # save_device() - - apps = device.get_apps() - - device.start_app(apps[0]) - - # Play media - device.play() + """Sends the command 'list' to the connected device.""" + self._send_command('List') diff --git a/sonyapilib/ssdp.py b/sonyapilib/ssdp.py index 188be9b..881942b 100644 --- a/sonyapilib/ssdp.py +++ b/sonyapilib/ssdp.py @@ -4,15 +4,12 @@ """ import socket -from http.client import HTTPResponse -from http.server import BaseHTTPRequestHandler from io import StringIO import email class SSDPResponse(): - """ - Holds the response of a ssdp request - """ + # pylint: disable=too-few-public-methods + """Holds the response of a ssdp request.""" def __init__(self, response): # pop the first line so we only process headers @@ -26,6 +23,7 @@ def __init__(self, response): headers = dict(message.items()) self.location = headers["LOCATION"] self.usn = headers["USN"] + # pylint: disable=invalid-name self.st = headers["ST"] self.cache = headers["CACHE-CONTROL"].split("=")[1] @@ -36,10 +34,12 @@ def __repr__(self): return "".format(**self.__dict__) class SSDPDiscovery(): - def discover(self, service="ssdp:all", timeout=1, retries=5, mx=3): - """ - Discovers the ssdp services. - """ + # pylint: disable=too-few-public-methods + """Discover devices via the ssdp protocol.""" + @staticmethod + def discover(service="ssdp:all", timeout=1, retries=5, mx=3): + # pylint: disable=invalid-name + """Discovers the ssdp services.""" socket.setdefaulttimeout(timeout) # fppp @@ -71,8 +71,3 @@ def discover(self, service="ssdp:all", timeout=1, retries=5, mx=3): except socket.timeout: break return list(responses.values()) - -if __name__ == '__main__': - disco = SSDPDiscovery(); - for service in disco.discover(): - print(service) diff --git a/tests/deviceTest.py b/tests/deviceTest.py index 6c69a92..dbd3317 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -1,49 +1,119 @@ import unittest +from unittest import mock import os.path from inspect import getsourcefile -import os.path as path, sys -current_dir = path.dirname(path.abspath(getsourcefile(lambda:0))) +import os.path as path +import sys +import requests + +current_dir = path.dirname(path.abspath(getsourcefile(lambda: 0))) sys.path.insert(0, current_dir[:current_dir.rfind(path.sep)]) from sonyapilib.device import SonyDevice sys.path.pop(0) -class TestHelper: +def read_file(file_name): + """ Reads a file from disk """ + __location__ = os.path.realpath(os.path.join( + os.getcwd(), os.path.dirname(__file__))) + with open(os.path.join(__location__, file_name)) as f: + return f.read() + + +def mocked_requests_get(*args, **kwargs): + class MockResponse: + def __init__(self, json_data, status_code, text=None): + self.json_data = json_data + self.status_code = status_code + self.text = text + + def json(self): + return self.json_data - @staticmethod - def read_file(file_name): - """ Reads a file from disk """ - __location__ = os.path.realpath(os.path.join( - os.getcwd(), os.path.dirname(__file__))) - with open(os.path.join(__location__, file_name)) as f: - return f.read() + def raise_for_status(self): + pass + + if args[0] == 'http://test:52323/dmr.xml': + return MockResponse(None, 200, read_file("xml/dmr_v3.xml")) + elif args[0] == 'http://someotherurl.com/anothertest.json': + return MockResponse({"key2": "value2"}, 200) + + return MockResponse(None, 404) class SonyDeviceTest(unittest.TestCase): - def test_parse_dmr_v3(self): - content = TestHelper.read_file("dmr_v3.xml") + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_parse_dmr_v3(self, mock_get): + content = read_file("xml/dmr_v3.xml") device = self.create_device() device._parse_dmr(content) self.verify_device_dmr(device) self.assertFalse(device.is_v4) - def test_parse_dmr_v4(self): - content = TestHelper.read_file("dmr_v4.xml") + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_parse_dmr_v4(self, mock_get): + content = read_file("xml/dmr_v4.xml") device = self.create_device() device._parse_dmr(content) self.verify_device_dmr(device) self.assertTrue(device.is_v4) - self.assertEqual(device.actions["register"].url, - 'http://192.168.178.23/sony/accessControl') + self.assertEqual(device.actions["register"].url, + 'http://192.168.178.23/sony/accessControl') self.assertEqual(device.actions["register"].mode, 4) - self.assertEqual(device.actions["getRemoteCommandList"].url, - 'http://192.168.178.23/sony/system') + self.assertEqual(device.actions["getRemoteCommandList"].url, + 'http://192.168.178.23/sony/system') - def test_parse_ircc(self): - content = TestHelper.read_file("ircc.xml") + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_parse_ircc(self, mock_get): + content = read_file("xml/ircc.xml") device = self.create_device() device._parse_ircc(content) + self.assertEqual(device.actionlist_url, + 'http://192.168.240.4:50002/actionList') + self.assertEqual(device.control_url, + 'http://test:50001/upnp/control/IRCC') + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_parse_action_list(self, mock_get): + content = read_file("xml/actionlist.xml") + device = self.create_device() + device._parse_action_list(content) + self.assertEqual(device.actions["register"].mode, 3) + actions = ["getText", + "sendText", + "getContentInformation", + "getSystemInformation", + "getRemoteCommandList", + "getStatus", + "getHistoryList", + "getContentUrl", + "sendContentUrl"] + base_url = "http://192.168.240.4:50002/" + for action in actions: + self.assertEqual(device.actions[action].url, base_url + action) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_parse_system_information(self, mock_get): + content = read_file("xml/getSysteminformation.xml") + device = self.create_device() + device._parse_system_information(content) + self.assertEqual(device.mac, "30-52-cb-cc-16-ee") + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_update_commands_v3(self, mock_get): + content = read_file("xml/getRemoteCommandList.xml") + device = self.create_device() + device._parse_command_list(content) + self.assertEqual(len(device.commands), 48) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_update_app_list(self, mock_get): + pass + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_recreate_authentication(self, mock_get): + pass def create_device(self): return SonyDevice("test", "test") diff --git a/tests/simple_example.py b/tests/simple_example.py new file mode 100644 index 0000000..1bfd817 --- /dev/null +++ b/tests/simple_example.py @@ -0,0 +1,41 @@ +import unittest +import os.path + +from inspect import getsourcefile +import os.path as path +import sys +current_dir = path.dirname(path.abspath(getsourcefile(lambda: 0))) +sys.path.insert(0, current_dir[:current_dir.rfind(path.sep)]) +from sonyapilib.device import SonyDevice, AuthenticationResult +sys.path.pop(0) + + +def register_device(device): + result = device.register() + if result != AuthenticationResult.PIN_NEEDED: + print("Error in registration") + return False + + pin = input("Enter the PIN displayed at your device: ") + device.send_authentication(pin) + return True + +if __name__ == "__main__": + + stored_config = "bluray.json" + device = None + + # device must be on for registration + host = "10.0.0.102" + device = SonyDevice(host, "SonyApiLib Python Test4") + if register_device(device): + # save_device() + apps = device.get_apps() + device.start_app(apps[0]) + + # Play media + device.play() + + + + \ No newline at end of file diff --git a/tests/xml/actionlist.xml b/tests/xml/actionlist.xml new file mode 100644 index 0000000..09c22b4 --- /dev/null +++ b/tests/xml/actionlist.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/dmr_v3.xml b/tests/xml/dmr_v3.xml similarity index 100% rename from tests/dmr_v3.xml rename to tests/xml/dmr_v3.xml diff --git a/tests/dmr_v4.xml b/tests/xml/dmr_v4.xml similarity index 100% rename from tests/dmr_v4.xml rename to tests/xml/dmr_v4.xml diff --git a/tests/xml/getRemoteCommandList.xml b/tests/xml/getRemoteCommandList.xml new file mode 100644 index 0000000..1882777 --- /dev/null +++ b/tests/xml/getRemoteCommandList.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/xml/getSysteminformation.xml b/tests/xml/getSysteminformation.xml new file mode 100644 index 0000000..51fdaa3 --- /dev/null +++ b/tests/xml/getSysteminformation.xml @@ -0,0 +1,39 @@ + + BDPlayer + 2015 + RMT-B119A + RMT-B120A + RMT-B122A + RMT-B123A + RMT-B126A + RMT-B119J + RMT-B127J + RMT-B119P + RMT-B120P + RMT-B121P + RMT-B122P + RMT-B127P + RMT-B119C + RMT-B120C + RMT-B122C + RMT-B127C + RMT-B127T + RMT-B115A + + + video + music + + + BD + DVD + CD + Net + + + + + + + + \ No newline at end of file diff --git a/tests/ircc.xml b/tests/xml/ircc.xml similarity index 100% rename from tests/ircc.xml rename to tests/xml/ircc.xml From d4ec7f1af7093ab77de3bcfcd29829b146b4436a Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:07:54 +0200 Subject: [PATCH 059/170] added pylint to travis --- .travis.yml | 1 + sonyapilib/device.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index f080143..b61f3d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,7 @@ python: - "3.6" cache: pip install: + - pip install pylint - pip install . script: - python tests/deviceTest.py diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 65b9fda..7aff2af 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -37,6 +37,7 @@ class HttpMethod(Enum): class XmlApiObject(): # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" + # todo check if commands, especially soap does work with v4. def __init__(self, xml_data): self.name = None From aca8902dd601a9c1af32afbfc185b8645b954369 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Wed, 3 Apr 2019 21:12:00 +0200 Subject: [PATCH 060/170] Refactor init of XmlApiObject --- sonyapilib/device.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 0a7afab..378ddf7 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -41,23 +41,10 @@ class XmlApiObject(): """ Holds data for a device action or a command """ def __init__(self, xml_data): - self.name = None - self.mode = None - self.url = None - self.type = None - self.value = None - self.mac = None - self.id = None + arttributes = ["name", "mode", "url", "type", "value", "mac", "id"] - if xml_data is not None: - for arg in self.__dict__: - if "_" in arg: - continue - if arg in xml_data: - if (arg == "mode"): - setattr(self, arg, int(xml_data[arg])) - else: - setattr(self, arg, xml_data[arg]) + for attr in arttributes: + setattr(self, attr, xml_data.get(attr)) class SonyDevice(): From b3da4ffba2756007e9fdeb4c6e651fcc4aa6aa52 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:23:00 +0200 Subject: [PATCH 061/170] Changed coveralls config. --- .coveralls.yaml | 2 -- .travis.yml | 9 ++++++--- test_requirements.txt | 4 ++++ 3 files changed, 10 insertions(+), 5 deletions(-) delete mode 100644 .coveralls.yaml create mode 100644 test_requirements.txt diff --git a/.coveralls.yaml b/.coveralls.yaml deleted file mode 100644 index 61eaf36..0000000 --- a/.coveralls.yaml +++ /dev/null @@ -1,2 +0,0 @@ -service_name: travis-pro -repo_token: 0LnUggY7qWBesuadHn8esHdbtDi45kAfi \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index b61f3d2..0a3bf68 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,11 @@ python: - "3.6" cache: pip install: - - pip install pylint - pip install . +before_script: + - pip install python-coveralls + - pip install -r test_requirements.txt --use-mirrors script: - - python tests/deviceTest.py - - pylint sonyapilib/*. \ No newline at end of file + - py.test tests/deviceTest.py --cov=deviceTest +after_success: + - coveralls \ No newline at end of file diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..ce34e81 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,4 @@ +pytest +pytest-pep8 +pytest-cov +git+https://github.com/melor/HTTPretty.git@py33 \ No newline at end of file From bfa472c6cc5ac4f16de00808ccf76544667b1a8d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:25:55 +0200 Subject: [PATCH 062/170] Removed invalid no mirrors option. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0a3bf68..0175d9c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ install: - pip install . before_script: - pip install python-coveralls - - pip install -r test_requirements.txt --use-mirrors + - pip install -r test_requirements.txt script: - py.test tests/deviceTest.py --cov=deviceTest after_success: From d87fa70244091429e287102ecf7e87e720ce777c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:33:38 +0200 Subject: [PATCH 063/170] changed travis. --- .gitignore | 4 +++- .travis.yml | 2 +- example.py | 7 ------- 3 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 example.py diff --git a/.gitignore b/.gitignore index 1d6d2bc..89ce0e4 100644 --- a/.gitignore +++ b/.gitignore @@ -106,4 +106,6 @@ venv.bak/ /site # mypy -.mypy_cache/ \ No newline at end of file +.mypy_cache/ + +.coveralls \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 0175d9c..0511f53 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,6 @@ before_script: - pip install python-coveralls - pip install -r test_requirements.txt script: - - py.test tests/deviceTest.py --cov=deviceTest + - py.test tests/*Test.py --cov=sonyapilib after_success: - coveralls \ No newline at end of file diff --git a/example.py b/example.py deleted file mode 100644 index 060d468..0000000 --- a/example.py +++ /dev/null @@ -1,7 +0,0 @@ -from sonyapilib.device import SonyDevice - -def save_device(): - data = device.save_to_json() - text_file = open("bluray.json", "w") - text_file.write(data) - text_file.close() From 302712d7f23c1bf55f072a1a7fd3d5be0d8f6b8c Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:37:45 +0200 Subject: [PATCH 064/170] travis. --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0511f53..91aab72 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,10 +3,9 @@ python: - "3.6" cache: pip install: - - pip install . -before_script: - pip install python-coveralls - pip install -r test_requirements.txt + - pip install . script: - py.test tests/*Test.py --cov=sonyapilib after_success: From 253452117cfac6a451a11803ce45d2455454fbf7 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:41:28 +0200 Subject: [PATCH 065/170] changed version for pytest. --- test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requirements.txt b/test_requirements.txt index ce34e81..f427087 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,4 +1,4 @@ -pytest +pytest>=3.6 pytest-pep8 pytest-cov git+https://github.com/melor/HTTPretty.git@py33 \ No newline at end of file From fa626f3688a00c70fb13cf2e1a9c7b651e0e5ac6 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:45:43 +0200 Subject: [PATCH 066/170] readded lint to travis. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 91aab72..09eed10 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,5 +8,5 @@ install: - pip install . script: - py.test tests/*Test.py --cov=sonyapilib -after_success: + - pylint tests/*Test.py - coveralls \ No newline at end of file From 051234df63b524c22c47d049425bea01ee10fe33 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:47:03 +0200 Subject: [PATCH 067/170] Changed readme to display v4 branch --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index eb69092..3f08b5a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Sonyapilib -[![Build Status](https://travis-ci.org/alexmohr/sonyapilib.svg?branch=master)](https://travis-ci.org/alexmohr/sonyapilib) -[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=master)](https://coveralls.io/github/alexmohr/sonyapilib?branch=master) +[![Build Status](https://travis-ci.org/alexmohr/sonyapilib.svg?branch=v4)](https://travis-ci.org/alexmohr/sonyapilib) +[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=master) + Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant wich you can find here: https://github.com/dilruacs/media_player.sony From 30c26b78f0f1e5f9e44db5f2ff7cd715589e77e5 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:48:11 +0200 Subject: [PATCH 068/170] updated test requirements. --- .travis.yml | 1 - test_requirements.txt | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 09eed10..35f0e16 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ python: - "3.6" cache: pip install: - - pip install python-coveralls - pip install -r test_requirements.txt - pip install . script: diff --git a/test_requirements.txt b/test_requirements.txt index f427087..77e462b 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,4 +1,6 @@ pytest>=3.6 pytest-pep8 pytest-cov +python-coveralls +pylint git+https://github.com/melor/HTTPretty.git@py33 \ No newline at end of file From 81e4f55e75f02de36f81a8413d6cd8b555348863 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 3 Apr 2019 21:53:00 +0200 Subject: [PATCH 069/170] corrected travis exec pylint. --- .travis.yml | 2 +- sonyapilib/device.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 35f0e16..586032d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,5 @@ install: - pip install . script: - py.test tests/*Test.py --cov=sonyapilib - - pylint tests/*Test.py + - pylint sonyapilib/*.py - coveralls \ No newline at end of file diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 7aff2af..60664f8 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -37,7 +37,6 @@ class HttpMethod(Enum): class XmlApiObject(): # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" - # todo check if commands, especially soap does work with v4. def __init__(self, xml_data): self.name = None @@ -63,6 +62,9 @@ def __init__(self, xml_data): class SonyDevice(): # pylint: disable=too-many-public-methods # pylint: disable=too-many-instance-attributes + # pylint: disable=fixme + # todo remove this again. + # todo check if commands, especially soap does work with v4. """Contains all data for the device.""" def __init__(self, host, nickname): @@ -333,6 +335,7 @@ def _request_json(self, url, params, log_errors=True): built_url = 'http://{}/{}'.format(self.host, url) try: + # todo refactor to use http send. response = requests.post(built_url, data=params.encode("UTF-8"), cookies=self.cookies, From 481c872b06e9ea10be2cfe438f93383ddd492764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Wed, 3 Apr 2019 22:02:08 +0200 Subject: [PATCH 070/170] Refactor and codestyling in device.py Some refactoring and codestyling in SonyDevice init, discover and _parse_ircc --- sonyapilib/device.py | 78 +++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 33 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 378ddf7..560b390 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -2,6 +2,7 @@ Sony Mediaplayer lib """ from enum import Enum +from urllib.urlparse import urljoin import base64 import collections import json @@ -40,11 +41,12 @@ class HttpMethod(Enum): class XmlApiObject(): """ Holds data for a device action or a command """ - def __init__(self, xml_data): + def __init__(self, xml_data={}): arttributes = ["name", "mode", "url", "type", "value", "mac", "id"] - for attr in arttributes: - setattr(self, attr, xml_data.get(attr)) + if xml_data: + for attr in arttributes: + setattr(self, attr, xml_data.get(attr)) class SonyDevice(): @@ -76,12 +78,12 @@ def __init__(self, host, nickname): self.mac = None self.is_v4 = False - self.ircc_url = "http://{0}:{1}/Ircc.xml".format(host, self.ircc_port) - self.irccscpd_url = "http://{0}:{1}/IRCCSCPD.xml".format( - host, self.ircc_port) - self.dmr_url = "http://{0}:{1}/dmr.xml".format( - self.host, self.dmr_port) - self.app_url = "http://{0}:{1}".format(self.host, self.app_port) + ircc_base = "http://{0.host}:{0.ircc_port}".format(self) + self.ircc_url = urljoin(ircc_base, "/Ircc.xml") + self.irccscpd_url = urljoin(ircc_base, "/IRCCSCPD.xml") + + self.dmr_url = "http://{0.host}:{0.dmr_port}/dmr.xml".format(self) + self.app_url = "http://{0.host}:{0.app_port}".format(self) @staticmethod def discover(): @@ -90,7 +92,9 @@ def discover(): # Todo check if this works with v4 discovery = ssdp.SSDPDiscovery() devices = [] - for device in discovery.discover("urn:schemas-sony-com:service:headersIRCC:1"): + for device in discovery.discover( + "urn:schemas-sony-com:service:headersIRCC:1" + ): host = device.location.split(":")[1].split("//")[1] devices.append(SonyDevice(host, device.location)) @@ -106,11 +110,11 @@ def save_to_json(self): return jsonpickle.dumps(self) def _update_service_urls(self): - """ Initialize the device by reading the necessary resources from it """ + """Initialize the device by reading the necessary resources from it """ response = self._send_http(self.dmr_url, method=HttpMethod.GET) if not response: _LOGGER.error("Failed to get DMR") - return + return None self._parse_dmr(response.text) @@ -119,19 +123,20 @@ def _update_service_urls(self): response = self._send_http(self.ircc_url, method=HttpMethod.GET) self._parse_ircc(response.text) - if len(self.commands) > 0: + if self.commands: self._update_commands() self._update_applist() def _parse_ircc(self, data): response = self._send_http(self.ircc_url, method=HttpMethod.GET) if not response: - return + return None xml_data = xml.etree.ElementTree.fromstring(data) # the action list contains everything the device supports - self.actionlist_url = xml_data.find("{0}device".format(URN_UPNP_DEVICE))\ + self.actionlist_url = xml_data.find( + "{0}device".format(URN_UPNP_DEVICE))\ .find("{0}X_UNR_DeviceInfo".format(URN_SONY_AV))\ .find("{0}X_CERS_ActionList_URL".format(URN_SONY_AV))\ .text @@ -165,9 +170,12 @@ def _parse_ircc(self, data): for service in services: service_id = service.find( "{0}serviceId".format(URN_UPNP_DEVICE)) - if not service_id: - continue - if "urn:schemas-sony-com:serviceId:IRCC" not in service_id.text: + + if any( + not service_id, + "urn:schemas-sony-com:serviceId:IRCC" + not in service_id.text + ): continue service_location = service.find( @@ -202,32 +210,36 @@ def _parse_dmr(self, data): transport_location = service.find( "{0}controlURL".format(URN_UPNP_DEVICE)).text self.av_transport_url = "{0}://{1}:{2}{3}".format( - lirc_url.scheme, lirc_url.netloc.split(":")[0], self.dmr_port, transport_location) + lirc_url.scheme, lirc_url.netloc.split(":")[0], + self.dmr_port, transport_location + ) # this is only for v4 devices. - if not "av:X_ScalarWebAPI_ServiceType" in data: - return + if "av:X_ScalarWebAPI_ServiceType" not in data: + return None self.is_v4 = True - deviceInfo = "{0}X_ScalarWebAPI_DeviceInfo"\ - .format(URN_SCALAR_WEB_API_DEVICE_INFO) + deviceInfo = "{0}X_ScalarWebAPI_DeviceInfo".format( + URN_SCALAR_WEB_API_DEVICE_INFO + ) for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): for deviceInfo in device.findall(deviceInfo): - base_url = deviceInfo.find("{0}X_ScalarWebAPI_BaseURL"\ - .format(URN_SCALAR_WEB_API_DEVICE_INFO))\ - .text - - action = XmlApiObject(None) - action.url = base_url + "/accessControl" + base_url = deviceInfo.find( + "{0}X_ScalarWebAPI_BaseURL".format( + URN_SCALAR_WEB_API_DEVICE_INFO + ) + ).text + + action = XmlApiObject() + action.url = urljoin(base_url, "/accessControl") action.mode = 4 self.actions["register"] = action - action = XmlApiObject(None) - action.url = base_url + "/system" + action = XmlApiObject() + action.url = urljoin(base_url, "/system") self.actions["getRemoteCommandList"] = action - def _update_commands(self): # needs to be registered to do that @@ -266,7 +278,7 @@ def _update_applist(self, log_errors=True): for app in apps: name = app.find("name").text id = app.find("id").text - data = XmlApiObject(None) + data = XmlApiObject() data.name = name data.id = id self.apps[name] = data From 8268a48cda037168fba6f81b12d13b1dcf22a031 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Wed, 3 Apr 2019 22:26:35 +0200 Subject: [PATCH 071/170] Fix urljoin in _parse_dmr --- sonyapilib/device.py | 8 +++++--- tests/dmr_v4.xml | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 560b390..b9f75fb 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -2,7 +2,7 @@ Sony Mediaplayer lib """ from enum import Enum -from urllib.urlparse import urljoin +from urllib.parse import urljoin import base64 import collections import json @@ -230,14 +230,16 @@ def _parse_dmr(self, data): URN_SCALAR_WEB_API_DEVICE_INFO ) ).text + if not base_url.endswith("/"): + base_url = "{}/".format(base_url) action = XmlApiObject() - action.url = urljoin(base_url, "/accessControl") + action.url = urljoin(base_url, "accessControl") action.mode = 4 self.actions["register"] = action action = XmlApiObject() - action.url = urljoin(base_url, "/system") + action.url = urljoin(base_url, "system") self.actions["getRemoteCommandList"] = action def _update_commands(self): diff --git a/tests/dmr_v4.xml b/tests/dmr_v4.xml index d8a8274..6ce8337 100644 --- a/tests/dmr_v4.xml +++ b/tests/dmr_v4.xml @@ -156,10 +156,10 @@ false 20677 - + http://192.168.178.23/DIAL/sony/applist B0:00:03:3F:5A:DE CoreTV_DIAL - \ No newline at end of file + From d479206368504d5e8c36746ad03e2c1a2a4e9ada Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Wed, 3 Apr 2019 23:16:37 +0200 Subject: [PATCH 072/170] Fix Bugs --- sonyapilib/device.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index bfda7de..63ad4ee 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -45,10 +45,12 @@ class XmlApiObject(): """Holds data for a device action or a command.""" def __init__(self, xml_data={}): - arttributes = ["name", "mode", "url", "type", "value", "mac", "id"] + attributes = ["name", "mode", "url", "type", "value", "mac", "id"] if xml_data: - for attr in arttributes: + for attr in attributes: + if attr == "mode" and xml_data.get(attr): + xml_data[attr] = int(xml_data[attr]) setattr(self, attr, xml_data.get(attr)) @@ -194,11 +196,11 @@ def _parse_ircc(self, data): service_id = service.find( "{0}serviceId".format(URN_UPNP_DEVICE)) - if any( - not service_id, + if any([ + service_id is None, "urn:schemas-sony-com:serviceId:IRCC" not in service_id.text - ): + ]): continue service_location = service.find( From 1d6a266da5eb6a7632c8a301f9793a099bd59e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Thu, 4 Apr 2019 00:11:33 +0200 Subject: [PATCH 073/170] Remove pylint warnings --- sonyapilib/device.py | 46 ++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 63ad4ee..83afcc9 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -2,13 +2,14 @@ Sony Mediaplayer lib """ from enum import Enum -from urllib.parse import urljoin +from urllib.parse import ( + urljoin, + urlparse, + quote, +) import base64 import json import logging -import socket -import struct -import urllib.parse import uuid import xml.etree.ElementTree @@ -43,15 +44,18 @@ class HttpMethod(Enum): class XmlApiObject(): # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" + name: str + mode: int + url: str + id: str - def __init__(self, xml_data={}): + def __init__(self, xml_data): attributes = ["name", "mode", "url", "type", "value", "mac", "id"] - if xml_data: - for attr in attributes: - if attr == "mode" and xml_data.get(attr): - xml_data[attr] = int(xml_data[attr]) - setattr(self, attr, xml_data.get(attr)) + for attr in attributes: + if attr == "mode" and xml_data.get(attr): + xml_data[attr] = int(xml_data[attr]) + setattr(self, attr, xml_data.get(attr)) class SonyDevice(): @@ -110,7 +114,7 @@ def discover(): discovery = ssdp.SSDPDiscovery() devices = [] for device in discovery.discover( - "urn:schemas-sony-com:service:headersIRCC:1" + "urn:schemas-sony-com:service:headersIRCC:1" ): host = device.location.split(":")[1].split("//")[1] devices.append(SonyDevice(host, device.location)) @@ -169,8 +173,8 @@ def _parse_action_list(self, data): # http://docs.python-requests.org/en/master/_modules/requests/api/?highlight=param action.url = "{0}?name={1}®istrationType=initial&deviceId={2}".format( action.url, - urllib.parse.quote(self.nickname), - urllib.parse.quote(self.get_device_id())) + quote(self.nickname), + quote(self.get_device_id())) if action.mode == 3: action.url = action.url + "&wolSupport=true" @@ -189,7 +193,7 @@ def _parse_ircc(self, data): .find("{0}serviceList".format(URN_UPNP_DEVICE))\ .findall("{0}service".format(URN_UPNP_DEVICE)) - lirc_url = urllib.parse.urlparse(self.ircc_url) + lirc_url = urlparse(self.ircc_url) if services: # read service list for service in services: @@ -197,9 +201,9 @@ def _parse_ircc(self, data): "{0}serviceId".format(URN_UPNP_DEVICE)) if any([ - service_id is None, - "urn:schemas-sony-com:serviceId:IRCC" - not in service_id.text + service_id is None, + "urn:schemas-sony-com:serviceId:IRCC" + not in service_id.text ]): continue @@ -217,7 +221,7 @@ def _parse_system_information(self, data): "functionItem").attrib["value"] def _parse_dmr(self, data): - lirc_url = urllib.parse.urlparse(self.ircc_url) + lirc_url = urlparse(self.ircc_url) xml_data = xml.etree.ElementTree.fromstring(data) for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): service_list = device.find( @@ -253,12 +257,12 @@ def _parse_dmr(self, data): if not base_url.endswith("/"): base_url = "{}/".format(base_url) - action = XmlApiObject() + action = XmlApiObject({}) action.url = urljoin(base_url, "accessControl") action.mode = 4 self.actions["register"] = action - action = XmlApiObject() + action = XmlApiObject({}) action.url = urljoin(base_url, "system") self.actions["getRemoteCommandList"] = action @@ -307,7 +311,7 @@ def _update_applist(self): for app in apps: name = app.find("name").text app_id = app.find("id").text - data = XmlApiObject(None) + data = XmlApiObject({}) data.name = name data.id = app_id self.apps[name] = data From b40a00761096a2cab5c2ad9bff6767b19097ec41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Thu, 4 Apr 2019 00:32:06 +0200 Subject: [PATCH 074/170] Fix pylint messages and merge errors --- setup.py | 2 +- sonyapilib/device.py | 34 ++++++++++++++++++---------------- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/setup.py b/setup.py index 632b9ff..102226c 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ setup( name='sonyapilib', packages=['sonyapilib'], # this must be the same as the name above - version='0.3.11', + version='0.4.0', description='Lib to control sony devices with their soap api', author='Alexander Mohr', author_email='sonyapilib@mohr.io', diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 83afcc9..1b15106 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -25,7 +25,9 @@ TIMEOUT = 5 URN_UPNP_DEVICE = "{urn:schemas-upnp-org:device-1-0}" URN_SONY_AV = "{urn:schemas-sony-com:av}" +URN_SONY_IRCC = "urn:schemas-sony-com:serviceId:IRCC" URN_SCALAR_WEB_API_DEVICE_INFO = "{urn:schemas-sony-com:av}" +WEBAPI_SERVICETYPE = "av:X_ScalarWebAPI_ServiceType" class AuthenticationResult(Enum): @@ -47,6 +49,7 @@ class XmlApiObject(): name: str mode: int url: str + value: str id: str def __init__(self, xml_data): @@ -131,11 +134,11 @@ def save_to_json(self): return jsonpickle.dumps(self) def _update_service_urls(self): - """Initialize the device by reading the necessary resources from it """ + """Initialize the device by reading the necessary resources from it.""" response = self._send_http(self.dmr_url, method=HttpMethod.GET) if not response: _LOGGER.error("Failed to get DMR") - return None + return self._parse_dmr(response.text) @@ -202,8 +205,7 @@ def _parse_ircc(self, data): if any([ service_id is None, - "urn:schemas-sony-com:serviceId:IRCC" - not in service_id.text + URN_SONY_IRCC not in service_id.text, ]): continue @@ -238,18 +240,18 @@ def _parse_dmr(self, data): self.dmr_port, transport_location ) - # this is only for v4 devices. - if "av:X_ScalarWebAPI_ServiceType" not in data: - return None + # this is only true for v4 devices. + if WEBAPI_SERVICETYPE not in data: + return self.is_v4 = True - deviceInfo = "{0}X_ScalarWebAPI_DeviceInfo".format( + device_info_name = "{0}X_ScalarWebAPI_DeviceInfo".format( URN_SCALAR_WEB_API_DEVICE_INFO ) for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): - for deviceInfo in device.findall(deviceInfo): - base_url = deviceInfo.find( + for device_info in device.findall(device_info_name): + base_url = device_info.find( "{0}X_ScalarWebAPI_BaseURL".format( URN_SCALAR_WEB_API_DEVICE_INFO ) @@ -264,6 +266,7 @@ def _parse_dmr(self, data): action = XmlApiObject({}) action.url = urljoin(base_url, "system") + action.value = "getRemoteControllerInfo" self.actions["getRemoteCommandList"] = action def _update_commands(self): @@ -309,12 +312,11 @@ def _update_applist(self): xml_data = xml.etree.ElementTree.fromstring(response.text) apps = xml_data.findall(".//app") for app in apps: - name = app.find("name").text - app_id = app.find("id").text - data = XmlApiObject({}) - data.name = name - data.id = app_id - self.apps[name] = data + data = XmlApiObject({ + "name": app.find("name").text, + "id": app.find("id").text, + }) + self.apps[data.name] = data def _recreate_authentication(self): """The default cookie is for URL/sony. For some commands we need it for the root path.""" From 074a341c3b668616d10aa0e396c07ddd1c5d3149 Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Thu, 4 Apr 2019 18:19:23 +0200 Subject: [PATCH 075/170] Add optional target for wakeonlan --- sonyapilib/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 1b15106..a5dc712 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -556,10 +556,10 @@ def send_authentication(self, pin): return False - def wakeonlan(self): + def wakeonlan(self, broadcast='255.255.255.255'): """Starts the device either via wakeonlan.""" if self.mac: - wakeonlan.send_magic_packet(self.mac, ip_address=self.host) + wakeonlan.send_magic_packet(self.mac, ip_address=broadcast) def get_playing_status(self): """Get the status of playback from the device""" From ec1de13b3769cfd1ca10bbb88ccb5b58ecd2e538 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 18:27:12 +0200 Subject: [PATCH 076/170] Unit tests --- .travis.yml | 2 +- sonyapilib/device.py | 81 ++++++++++++----------- sonyapilib/ssdp.py | 2 + tests/deviceTest.py | 152 ++++++++++++++++++++++++++++++++++++------- 4 files changed, 174 insertions(+), 63 deletions(-) diff --git a/.travis.yml b/.travis.yml index 586032d..37b27ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,5 +7,5 @@ install: - pip install . script: - py.test tests/*Test.py --cov=sonyapilib - - pylint sonyapilib/*.py + - pylint sonyapilib - coveralls \ No newline at end of file diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 1b15106..973d799 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -117,7 +117,7 @@ def discover(): discovery = ssdp.SSDPDiscovery() devices = [] for device in discovery.discover( - "urn:schemas-sony-com:service:headersIRCC:1" + "urn:schemas-sony-com:service:IRCC:1" ): host = device.location.split(":")[1].split("//")[1] devices.append(SonyDevice(host, device.location)) @@ -144,28 +144,24 @@ def _update_service_urls(self): try: if self.is_v4: + # todo implement this pass else: - response = self._send_http( - self.ircc_url, method=HttpMethod.GET) - if response: - self._parse_ircc(response.text) - - response = self._send_http( - self.actionlist_url, method=HttpMethod.GET) - if response: - self._parse_action_list(response.text) - - response = self._send_http( - self._get_action("getSystemInformation").url, method=HttpMethod.GET) - if response: - self._parse_system_information(response.text) + self._parse_ircc() + self._parse_action_list() + self._parse_system_information() + except Exception as ex: # pylint: disable=broad-except _LOGGER.error("failed to get device information: %s", str(ex)) - def _parse_action_list(self, data): - xml_data = xml.etree.ElementTree.fromstring(data) + def _parse_action_list(self): + response = self._send_http( + self.actionlist_url, method=HttpMethod.GET) + if not response: + return + + xml_data = xml.etree.ElementTree.fromstring(response.text) for element in xml_data.findall("action"): action = XmlApiObject(element.attrib) self.actions[action.name] = action @@ -182,8 +178,13 @@ def _parse_action_list(self, data): if action.mode == 3: action.url = action.url + "&wolSupport=true" - def _parse_ircc(self, data): - xml_data = xml.etree.ElementTree.fromstring(data) + def _parse_ircc(self): + response = self._send_http( + self.ircc_url, method=HttpMethod.GET) + if not response: + return + + xml_data = xml.etree.ElementTree.fromstring(response.text) # the action list contains everything the device supports self.actionlist_url = xml_data.find( @@ -214,8 +215,13 @@ def _parse_ircc(self, data): service_url = lirc_url.scheme + "://" + lirc_url.netloc self.control_url = service_url + service_location - def _parse_system_information(self, data): - xml_data = xml.etree.ElementTree.fromstring(data) + def _parse_system_information(self): + response = self._send_http( + self._get_action("getSystemInformation").url, method=HttpMethod.GET) + if not response: + return + + xml_data = xml.etree.ElementTree.fromstring(response.text) for element in xml_data.findall("supportFunction"): for function in element.findall("function"): if function.attrib["name"] == "WOL": @@ -274,18 +280,12 @@ def _update_commands(self): # need to be registered to do that if not self.pin: - _LOGGER.info("Registration necessary to read command list.") + _LOGGER.error("Registration necessary to read command list.") return - - url = self._get_action("getRemoteCommandList").url - if self._get_action("register").mode < 4: - response = self._send_http(url, method=HttpMethod.GET) - if response: - self._parse_command_list(response.text) - else: - _LOGGER.error("Failed to get response") - else: - action_name = "getRemoteCommandList" + + if self.is_v4: + # todo refactor to method + action_name = "getRemoteControllerInfo" action = self.actions[action_name] json_data = self._create_api_json(action.value) @@ -296,9 +296,17 @@ def _update_commands(self): else: _LOGGER.error("JSON request error: %s", json.dumps(resp, indent=4)) + else: + self._parse_command_list() + + def _parse_command_list(self): + url = self._get_action("getRemoteCommandList").url + response = self._send_http(url, method=HttpMethod.GET) + if not response: + _LOGGER.error("Failed to get response for command list") + return - def _parse_command_list(self, data): - xml_data = xml.etree.ElementTree.fromstring(data) + xml_data = xml.etree.ElementTree.fromstring(response.text) for command in xml_data.findall("command"): name = command.get("name") self.commands[name] = XmlApiObject(command.attrib) @@ -426,11 +434,6 @@ def _send_http(self, url, method, data=None, headers=None, log_errors=True, rais _LOGGER.error("HTTPError: %s", str(ex)) if raise_errors: raise - except Exception as ex: # pylint: disable=broad-except - if log_errors: - _LOGGER.error("Exception: %s", str(ex)) - if raise_errors: - raise else: return response diff --git a/sonyapilib/ssdp.py b/sonyapilib/ssdp.py index 881942b..d0dcd07 100644 --- a/sonyapilib/ssdp.py +++ b/sonyapilib/ssdp.py @@ -12,6 +12,8 @@ class SSDPResponse(): """Holds the response of a ssdp request.""" def __init__(self, response): + if not response: + return # pop the first line so we only process headers # first line is http response _, headers = response.split('\r\n', 1) diff --git a/tests/deviceTest.py b/tests/deviceTest.py index dbd3317..90c05b0 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -7,11 +7,23 @@ import sys import requests + current_dir = path.dirname(path.abspath(getsourcefile(lambda: 0))) sys.path.insert(0, current_dir[:current_dir.rfind(path.sep)]) -from sonyapilib.device import SonyDevice +# cannot be imported at a different position because path modification +# is necessary to load the local library. +# otherwise it must be installed after every change +from sonyapilib.device import SonyDevice, XmlApiObject +from sonyapilib.ssdp import SSDPResponse sys.path.pop(0) + +ACTION_LIST_URL = 'http://192.168.240.4:50002/actionList' +DMR_URL = 'http://test:52323/dmr.xml' +IRCC_URL = 'http://test:50001/Ircc.xml' +SYSTEM_INFORMATION_URL = 'http://192.168.240.4:50002/getSystemInformation' +GET_REMOTE_COMMAND_LIST_URL = 'http://192.168.240.4:50002/getRemoteCommandList' + def read_file(file_name): """ Reads a file from disk """ __location__ = os.path.realpath(os.path.join( @@ -20,6 +32,16 @@ def read_file(file_name): return f.read() +def mock_nothing(*args, **kwargs): + pass + +def mock_discovery(*args, **kwargs): + if args[0] == "urn:schemas-sony-com:service:IRCC:1": + resp = SSDPResponse(None) + resp.location = IRCC_URL + return [resp] + return None + def mocked_requests_get(*args, **kwargs): class MockResponse: def __init__(self, json_data, status_code, text=None): @@ -32,17 +54,83 @@ def json(self): def raise_for_status(self): pass - - if args[0] == 'http://test:52323/dmr.xml': + url = args[0] + print(url) + if url == DMR_URL: return MockResponse(None, 200, read_file("xml/dmr_v3.xml")) - elif args[0] == 'http://someotherurl.com/anothertest.json': - return MockResponse({"key2": "value2"}, 200) + elif url == IRCC_URL: + return MockResponse(None, 200, read_file("xml/ircc.xml")) + elif url == ACTION_LIST_URL: + return MockResponse(None, 200, read_file("xml/actionlist.xml")) + elif url == SYSTEM_INFORMATION_URL: + return MockResponse(None, 200, read_file("xml/getSysteminformation.xml")) + elif url == GET_REMOTE_COMMAND_LIST_URL: + return MockResponse(None, 200, read_file("xml/getRemoteCommandList.xml")) + # elif url == 'http://someotherurl.com/anothertest.json': + # return MockResponse({"key2": "value2"}, 200) return MockResponse(None, 404) class SonyDeviceTest(unittest.TestCase): + @mock.patch('sonyapilib.device.SonyDevice._update_service_urls', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._recreate_authentication', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._update_commands', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._update_applist', side_effect=mock_nothing) + def test_init_device_no_pin(self, mock_update_applist, mock_update_command, + mock_recreate_auth, mock_update_service_url): + device = self.create_device() + device._init_device() + self.assertEquals(mock_update_service_url.call_count, 1) + self.assertEquals(mock_recreate_auth.call_count, 0) + self.assertEquals(mock_update_command.call_count, 0) + self.assertEquals(mock_update_applist.call_count, 0) + + @mock.patch('sonyapilib.device.SonyDevice._update_service_urls', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._recreate_authentication', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._update_commands', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._update_applist', side_effect=mock_nothing) + def test_init_device_with_pin(self, mock_update_applist, mock_update_command, + mock_recreate_auth, mock_update_service_url): + device = self.create_device() + device.pin = 1234 + device._init_device() + self.assertEquals(mock_update_service_url.call_count, 1) + self.assertEquals(mock_recreate_auth.call_count, 1) + self.assertEquals(mock_update_command.call_count, 1) + self.assertEquals(mock_update_applist.call_count, 1) + + @mock.patch('sonyapilib.ssdp.SSDPDiscovery.discover', side_effect=mock_discovery) + def test_discovery(self, mock_discover): + devices = SonyDevice.discover() + self.assertEquals(len(devices), 1) + self.assertEquals(devices[0].host, "test") + + def test_save_load_from_json(self): + device = self.create_device() + jdata = device.save_to_json() + restored_device = SonyDevice.load_from_json(jdata) + jdata_restored = restored_device.save_to_json() + self.assertEquals(jdata, jdata_restored) + + def test_update_service_urls_v4(self): + # todo + pass + + @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch('sonyapilib.device.SonyDevice._parse_ircc', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._parse_action_list', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._parse_system_information', side_effect=mock_nothing) + def test_update_service_urls_v3(self, mock_ircc, mock_action_list, + mock_system_information, mocked_requests_get): + device = self.create_device() + device.pin = 1234 + device._update_service_urls() + self.assertEquals(mock_ircc.call_count, 1) + self.assertEquals(mock_action_list.call_count, 1) + self.assertEquals(mock_system_information.call_count, 1) + @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_dmr_v3(self, mock_get): content = read_file("xml/dmr_v3.xml") @@ -58,27 +146,27 @@ def test_parse_dmr_v4(self, mock_get): device._parse_dmr(content) self.verify_device_dmr(device) self.assertTrue(device.is_v4) - self.assertEqual(device.actions["register"].url, - 'http://192.168.178.23/sony/accessControl') + self.assertEqual( + device.actions["register"].url, 'http://192.168.178.23/sony/accessControl') self.assertEqual(device.actions["register"].mode, 4) - self.assertEqual(device.actions["getRemoteCommandList"].url, - 'http://192.168.178.23/sony/system') + self.assertEqual( + device.actions["getRemoteCommandList"].url, 'http://192.168.178.23/sony/system') @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_ircc(self, mock_get): - content = read_file("xml/ircc.xml") device = self.create_device() - device._parse_ircc(content) - self.assertEqual(device.actionlist_url, - 'http://192.168.240.4:50002/actionList') - self.assertEqual(device.control_url, - 'http://test:50001/upnp/control/IRCC') + device._parse_ircc() + self.assertEqual( + device.actionlist_url, 'http://192.168.240.4:50002/actionList') + self.assertEqual( + device.control_url, 'http://test:50001/upnp/control/IRCC') @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_action_list(self, mock_get): - content = read_file("xml/actionlist.xml") device = self.create_device() - device._parse_action_list(content) + # must be set before prior methods are not called. + device.actionlist_url = ACTION_LIST_URL + device._parse_action_list() self.assertEqual(device.actions["register"].mode, 3) actions = ["getText", "sendText", @@ -95,18 +183,36 @@ def test_parse_action_list(self, mock_get): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_system_information(self, mock_get): - content = read_file("xml/getSysteminformation.xml") device = self.create_device() - device._parse_system_information(content) + data = XmlApiObject({}) + data.url = SYSTEM_INFORMATION_URL + device.actions["getSystemInformation"] = data + device._parse_system_information() self.assertEqual(device.mac, "30-52-cb-cc-16-ee") - + @mock.patch('requests.get', side_effect=mocked_requests_get) - def test_update_commands_v3(self, mock_get): - content = read_file("xml/getRemoteCommandList.xml") + def test_parse_command_list(self, mock_get): device = self.create_device() - device._parse_command_list(content) + device._parse_command_list() self.assertEqual(len(device.commands), 48) + @mock.patch('sonyapilib.device.SonyDevice._parse_command_list', side_effect=mock_nothing) + def test_update_commands_no_pin(self, mock_parse_cmd_list): + device = self.create_device() + device._update_commands() + self.assertEquals(mock_parse_cmd_list.call_count, 0) + + @mock.patch('sonyapilib.device.SonyDevice._parse_command_list', side_effect=mock_nothing) + def test_update_commands_v3(self, mock_parse_cmd_list): + device = self.create_device() + device.pin = 1234 + device._update_commands() + self.assertEquals(mock_parse_cmd_list.call_count, 1) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_update_commands_v4(self, mock_get): + pass + @mock.patch('requests.get', side_effect=mocked_requests_get) def test_update_app_list(self, mock_get): pass From 79e7d3617684ceb770037e90f144765e4f33abce Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 18:30:23 +0200 Subject: [PATCH 077/170] linter --- sonyapilib/device.py | 5 ++--- tests/deviceTest.py | 3 +++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 73bf3b9..35f2777 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -150,7 +150,6 @@ def _update_service_urls(self): self._parse_ircc() self._parse_action_list() self._parse_system_information() - except Exception as ex: # pylint: disable=broad-except _LOGGER.error("failed to get device information: %s", str(ex)) @@ -282,7 +281,7 @@ def _update_commands(self): if not self.pin: _LOGGER.error("Registration necessary to read command list.") return - + if self.is_v4: # todo refactor to method action_name = "getRemoteControllerInfo" @@ -298,7 +297,7 @@ def _update_commands(self): json.dumps(resp, indent=4)) else: self._parse_command_list() - + def _parse_command_list(self): url = self._get_action("getRemoteCommandList").url response = self._send_http(url, method=HttpMethod.GET) diff --git a/tests/deviceTest.py b/tests/deviceTest.py index 90c05b0..3102b4f 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -193,6 +193,9 @@ def test_parse_system_information(self, mock_get): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_command_list(self, mock_get): device = self.create_device() + data = XmlApiObject({}) + data.url = GET_REMOTE_COMMAND_LIST_URL + device.actions["getRemoteCommandList"] = data device._parse_command_list() self.assertEqual(len(device.commands), 48) From 77343b888f79c56f4d6e7e489f1df34eaab9ba8c Mon Sep 17 00:00:00 2001 From: Edwin Top Date: Thu, 4 Apr 2019 18:39:18 +0200 Subject: [PATCH 078/170] Add broadcast as optional to power method --- sonyapilib/device.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index a5dc712..9045e64 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -618,10 +618,10 @@ def start_app(self, app_name): data = "LOCATION: {0}/run".format(url) self._send_http(url, HttpMethod.POST, data=data) - def power(self, power_on): + def power(self, power_on, broadcast=None): """Powers the device on or shuts it off.""" if power_on: - self.wakeonlan() + self.wakeonlan(broadcast) # Try using the power on command incase the WOL doesn't work if not self.get_power_status(): # Try using the power on command incase the WOL doesn't work From f13dcfd7193b68af707b410d40842540214ab32e Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 18:41:28 +0200 Subject: [PATCH 079/170] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3f08b5a..b8d5526 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sonyapilib [![Build Status](https://travis-ci.org/alexmohr/sonyapilib.svg?branch=v4)](https://travis-ci.org/alexmohr/sonyapilib) -[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=v4) Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant wich you can find here: From 248fc1428be11a76afcb18d22a327e6f903b700e Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 19:04:32 +0200 Subject: [PATCH 080/170] tests --- README.md | 2 +- sonyapilib/device.py | 7 +++---- tests/deviceTest.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3f08b5a..b8d5526 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sonyapilib [![Build Status](https://travis-ci.org/alexmohr/sonyapilib.svg?branch=v4)](https://travis-ci.org/alexmohr/sonyapilib) -[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=master) +[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=v4) Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant wich you can find here: diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 35f2777..1645ed8 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -140,9 +140,8 @@ def _update_service_urls(self): _LOGGER.error("Failed to get DMR") return - self._parse_dmr(response.text) - try: + self._parse_dmr(response.text) if self.is_v4: # todo implement this pass @@ -428,7 +427,7 @@ def _send_http(self, url, method, data=None, headers=None, log_errors=True, rais timeout=TIMEOUT) response.raise_for_status() - except requests.exceptions.HTTPError as ex: + except requests.exceptions.RequestException as ex: if log_errors: _LOGGER.error("HTTPError: %s", str(ex)) if raise_errors: @@ -477,7 +476,7 @@ def register(self): Register at the api. The name which will be displayed in the UI of the device. Make sure this name does not exist yet For this the device must be put in registration mode. - The tested sd5500 has no separate mode but allows registration in the overview " + The tested sd5500 has no separate mode but allows registration in the overview """ registration_result = AuthenticationResult.ERROR diff --git a/tests/deviceTest.py b/tests/deviceTest.py index 3102b4f..d4c1eda 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -31,6 +31,8 @@ def read_file(file_name): with open(os.path.join(__location__, file_name)) as f: return f.read() +def mock_error(*args, **kwargs): + raise Exception() def mock_nothing(*args, **kwargs): pass @@ -114,6 +116,17 @@ def test_save_load_from_json(self): jdata_restored = restored_device.save_to_json() self.assertEquals(jdata, jdata_restored) + def test_update_service_urls_error_response(self): + device = self.create_device() + device._update_service_urls() + + @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch('sonyapilib.device.SonyDevice._parse_ircc', side_effect=mock_error) + def test_update_service_urls_error_processing(self, mock_error, mocked_requests_get): + device = self.create_device() + device._update_service_urls() + self.assertEquals(mock_error.call_count, 1) + def test_update_service_urls_v4(self): # todo pass From 4292d043b5ae58c6addbe2e790d3e3d482100129 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 19:53:47 +0200 Subject: [PATCH 081/170] Changed python version to 3.5 to validate errors in #27 --- .travis.yml | 2 +- sonyapilib/device.py | 51 ++++++++++++------------ tests/deviceTest.py | 93 +++++++++++++++++++++++++++++++++----------- 3 files changed, 98 insertions(+), 48 deletions(-) diff --git a/.travis.yml b/.travis.yml index 37b27ac..608b620 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "3.6" + - "3.5" cache: pip install: - pip install -r test_requirements.txt diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 8bd2e62..6ea225e 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -46,11 +46,11 @@ class HttpMethod(Enum): class XmlApiObject(): # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" - name: str - mode: int - url: str - value: str - id: str + name: None + mode: None + url: None + value: None + id: None def __init__(self, xml_data): attributes = ["name", "mode", "url", "type", "value", "mac", "id"] @@ -466,6 +466,27 @@ def _send_req_ircc(self, params): url=self.control_url, params=data, action=action) return content + + def _send_command(self, name): + if not self.commands: + self._init_device() + + if self.commands: + if name in self.commands: + self._send_req_ircc(self.commands[name].value) + else: + raise ValueError('Unknown command: %s' % name) + else: + raise ValueError('Failed to read command list from device.') + + def _get_action(self, name): + """Get the action object for the action with the given name""" + if name not in self.actions and not self.actions: + if name not in self.actions and not self.actions: + raise ValueError('Failed to read action list from device.') + + return self.actions[name] + def get_device_id(self): """Returns the id which is used for the registration.""" return "TVSideView:{0}".format(self.uuid) @@ -590,26 +611,6 @@ def get_power_status(self): return False return True - def _send_command(self, name): - if not self.commands: - self._init_device() - - if self.commands: - if name in self.commands: - self._send_req_ircc(self.commands[name].value) - else: - raise ValueError('Unknown command: %s' % name) - else: - raise ValueError('Failed to read command list from device.') - - def _get_action(self, name): - """Get the action object for the action with the given name""" - if name not in self.actions and not self.actions: - if name not in self.actions and not self.actions: - raise ValueError('Failed to read action list from device.') - - return self.actions[name] - def start_app(self, app_name): """Start an app by name""" # sometimes device does not start app if already running one diff --git a/tests/deviceTest.py b/tests/deviceTest.py index d4c1eda..2e99a89 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -13,7 +13,7 @@ # cannot be imported at a different position because path modification # is necessary to load the local library. # otherwise it must be installed after every change -from sonyapilib.device import SonyDevice, XmlApiObject +from sonyapilib.device import SonyDevice, XmlApiObject, AuthenticationResult from sonyapilib.ssdp import SSDPResponse sys.path.pop(0) @@ -23,6 +23,8 @@ IRCC_URL = 'http://test:50001/Ircc.xml' SYSTEM_INFORMATION_URL = 'http://192.168.240.4:50002/getSystemInformation' GET_REMOTE_COMMAND_LIST_URL = 'http://192.168.240.4:50002/getRemoteCommandList' +REGISTRATION_URL_LEGACY = 'http://192.168.240.4:50002/register' +REGISTRATION_URL_V4 = 'http://192.168.178.23/sony/accessControl' def read_file(file_name): """ Reads a file from disk """ @@ -84,10 +86,10 @@ def test_init_device_no_pin(self, mock_update_applist, mock_update_command, mock_recreate_auth, mock_update_service_url): device = self.create_device() device._init_device() - self.assertEquals(mock_update_service_url.call_count, 1) - self.assertEquals(mock_recreate_auth.call_count, 0) - self.assertEquals(mock_update_command.call_count, 0) - self.assertEquals(mock_update_applist.call_count, 0) + self.assertEqual(mock_update_service_url.call_count, 1) + self.assertEqual(mock_recreate_auth.call_count, 0) + self.assertEqual(mock_update_command.call_count, 0) + self.assertEqual(mock_update_applist.call_count, 0) @mock.patch('sonyapilib.device.SonyDevice._update_service_urls', side_effect=mock_nothing) @mock.patch('sonyapilib.device.SonyDevice._recreate_authentication', side_effect=mock_nothing) @@ -98,23 +100,23 @@ def test_init_device_with_pin(self, mock_update_applist, mock_update_command, device = self.create_device() device.pin = 1234 device._init_device() - self.assertEquals(mock_update_service_url.call_count, 1) - self.assertEquals(mock_recreate_auth.call_count, 1) - self.assertEquals(mock_update_command.call_count, 1) - self.assertEquals(mock_update_applist.call_count, 1) + self.assertEqual(mock_update_service_url.call_count, 1) + self.assertEqual(mock_recreate_auth.call_count, 1) + self.assertEqual(mock_update_command.call_count, 1) + self.assertEqual(mock_update_applist.call_count, 1) @mock.patch('sonyapilib.ssdp.SSDPDiscovery.discover', side_effect=mock_discovery) def test_discovery(self, mock_discover): devices = SonyDevice.discover() - self.assertEquals(len(devices), 1) - self.assertEquals(devices[0].host, "test") + self.assertEqual(len(devices), 1) + self.assertEqual(devices[0].host, "test") def test_save_load_from_json(self): device = self.create_device() jdata = device.save_to_json() restored_device = SonyDevice.load_from_json(jdata) jdata_restored = restored_device.save_to_json() - self.assertEquals(jdata, jdata_restored) + self.assertEqual(jdata, jdata_restored) def test_update_service_urls_error_response(self): device = self.create_device() @@ -125,7 +127,7 @@ def test_update_service_urls_error_response(self): def test_update_service_urls_error_processing(self, mock_error, mocked_requests_get): device = self.create_device() device._update_service_urls() - self.assertEquals(mock_error.call_count, 1) + self.assertEqual(mock_error.call_count, 1) def test_update_service_urls_v4(self): # todo @@ -140,9 +142,9 @@ def test_update_service_urls_v3(self, mock_ircc, mock_action_list, device = self.create_device() device.pin = 1234 device._update_service_urls() - self.assertEquals(mock_ircc.call_count, 1) - self.assertEquals(mock_action_list.call_count, 1) - self.assertEquals(mock_system_information.call_count, 1) + self.assertEqual(mock_ircc.call_count, 1) + self.assertEqual(mock_action_list.call_count, 1) + self.assertEqual(mock_system_information.call_count, 1) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_dmr_v3(self, mock_get): @@ -164,6 +166,10 @@ def test_parse_dmr_v4(self, mock_get): self.assertEqual(device.actions["register"].mode, 4) self.assertEqual( device.actions["getRemoteCommandList"].url, 'http://192.168.178.23/sony/system') + + def test_parse_ircc_error(self): + device = self.create_device() + device._parse_ircc() @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_ircc(self, mock_get): @@ -203,12 +209,20 @@ def test_parse_system_information(self, mock_get): device._parse_system_information() self.assertEqual(device.mac, "30-52-cb-cc-16-ee") - @mock.patch('requests.get', side_effect=mocked_requests_get) - def test_parse_command_list(self, mock_get): + def prepare_test_command_list(self): device = self.create_device() data = XmlApiObject({}) data.url = GET_REMOTE_COMMAND_LIST_URL device.actions["getRemoteCommandList"] = data + return device + + def test_parse_command_list_error(self): + device = self.prepare_test_command_list() + device._parse_command_list() + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_parse_command_list(self, mock_get): + device = self.prepare_test_command_list() device._parse_command_list() self.assertEqual(len(device.commands), 48) @@ -216,14 +230,14 @@ def test_parse_command_list(self, mock_get): def test_update_commands_no_pin(self, mock_parse_cmd_list): device = self.create_device() device._update_commands() - self.assertEquals(mock_parse_cmd_list.call_count, 0) + self.assertEqual(mock_parse_cmd_list.call_count, 0) @mock.patch('sonyapilib.device.SonyDevice._parse_command_list', side_effect=mock_nothing) def test_update_commands_v3(self, mock_parse_cmd_list): device = self.create_device() device.pin = 1234 device._update_commands() - self.assertEquals(mock_parse_cmd_list.call_count, 1) + self.assertEqual(mock_parse_cmd_list.call_count, 1) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_update_commands_v4(self, mock_get): @@ -233,10 +247,45 @@ def test_update_commands_v4(self, mock_get): def test_update_app_list(self, mock_get): pass - @mock.patch('requests.get', side_effect=mocked_requests_get) - def test_recreate_authentication(self, mock_get): + def test_recreate_authentication_v3(self): + device = self.create_device() + self.add_register_to_device(device, 3) + device._recreate_authentication() + + self.assertEqual(device.headers["Authorization"], "Basic OjEyMzQ=") + self.assertEqual(device.headers["X-CERS-DEVICE-ID"], device.get_device_id()) + + def test_recreate_authentication_v4(self): + device = self.create_device() + self.add_register_to_device(device, 4) + device._recreate_authentication() + + self.assertEqual(device.headers["Authorization"], "Basic OjEyMzQ=") + self.assertEqual(device.headers["Connection"], "keep-alive") + + def test_recreate_authentication_v4_psk(self): + # todo implement psk pass + @unittest.skip + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_register_v1(self, mocked_get): + device = self.create_device() + self.add_register_to_device(device, 1) + result = device.register() + self.assertEqual(result, AuthenticationResult.SUCCESS) + + def add_register_to_device(self, device, mode): + register_action = XmlApiObject({}) + register_action.mode = mode + if mode < 4: + register_action.url = REGISTRATION_URL_LEGACY + else: + register_action.url = REGISTRATION_URL_V4 + device.actions["register"] = register_action + device.pin = 1234 + + def create_device(self): return SonyDevice("test", "test") From f8e2beff42f6e4b925bfd19596cb71c9a5253920 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 20:02:58 +0200 Subject: [PATCH 082/170] partially rollback of XmlApiObject to solve #27 --- sonyapilib/device.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 6ea225e..98e2691 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -46,21 +46,22 @@ class HttpMethod(Enum): class XmlApiObject(): # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" - name: None - mode: None - url: None - value: None - id: None - def __init__(self, xml_data): - attributes = ["name", "mode", "url", "type", "value", "mac", "id"] + self.name = None + self.mode = None + self.url = None + self.type = None + self.value = None + self.mac = None + self.id = None + if not xml_data: + return - for attr in attributes: + for attr in self.__dict__: if attr == "mode" and xml_data.get(attr): - xml_data[attr] = int(xml_data[attr]) + xml_data[attr] = int(xml_data[attr]) setattr(self, attr, xml_data.get(attr)) - class SonyDevice(): # pylint: disable=too-many-public-methods # pylint: disable=too-many-instance-attributes From 0db9458ac4ed2aed4772d8675d8d3c33a4625a1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Thu, 4 Apr 2019 18:46:05 +0200 Subject: [PATCH 083/170] Add function to find in xml --- sonyapilib/device.py | 126 ++++++++++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 42 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 1b15106..a806428 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -30,6 +30,42 @@ WEBAPI_SERVICETYPE = "av:X_ScalarWebAPI_ServiceType" +def xml_search_helper(data, param): + '''Performs find or findall on given xml with string from param.''' + if isinstance(param, (tuple, list)) and param[1] == "all": + result = data.findall(param[0]) + else: + result = data.find(param) + return result + + +def iterate_search_data(data, param): + '''Search in nested lists.''' + result = [] + for element in data: + if isinstance(element, list): + result.append(iterate_search_data(element, param)) + else: + result.append(xml_search_helper(element, param)) + return result + + +def find_in_xml(data, search_params): + '''Takes an xml from string or as xml.etree.ElementTree and an iterable of + strings (or tuple in case of findall) to search.''' + if isinstance(data, str): + data = xml.etree.ElementTree.fromstring(data) + param = search_params[0] + if isinstance(data, list): + result = iterate_search_data(data, param) + else: + result = xml_search_helper(data, param) + + if len(search_params) == 1: + return result + return find_in_xml(result, search_params[1:]) + + class AuthenticationResult(Enum): """Stores the result of the authentication process.""" SUCCESS = 0 @@ -165,8 +201,7 @@ def _update_service_urls(self): _LOGGER.error("failed to get device information: %s", str(ex)) def _parse_action_list(self, data): - xml_data = xml.etree.ElementTree.fromstring(data) - for element in xml_data.findall("action"): + for element in find_in_xml(data, [("action", "all")]): action = XmlApiObject(element.attrib) self.actions[action.name] = action @@ -183,41 +218,42 @@ def _parse_action_list(self, data): action.url = action.url + "&wolSupport=true" def _parse_ircc(self, data): - xml_data = xml.etree.ElementTree.fromstring(data) - + upnp_device = "{}device".format(URN_UPNP_DEVICE) # the action list contains everything the device supports - self.actionlist_url = xml_data.find( - "{0}device".format(URN_UPNP_DEVICE))\ - .find("{0}X_UNR_DeviceInfo".format(URN_SONY_AV))\ - .find("{0}X_CERS_ActionList_URL".format(URN_SONY_AV))\ - .text - - services = xml_data.find("{0}device".format(URN_UPNP_DEVICE))\ - .find("{0}serviceList".format(URN_UPNP_DEVICE))\ - .findall("{0}service".format(URN_UPNP_DEVICE)) + self.actionlist_url = find_in_xml( + data, + [upnp_device, + "{}X_UNR_DeviceInfo".format(URN_SONY_AV), + "{}X_CERS_ActionList_URL".format(URN_SONY_AV)] + ).text + services = find_in_xml( + data, + [upnp_device, + "{}serviceList".format(URN_UPNP_DEVICE), + ("{}service".format(URN_UPNP_DEVICE), "all")], + ) lirc_url = urlparse(self.ircc_url) - if services: - # read service list - for service in services: - service_id = service.find( - "{0}serviceId".format(URN_UPNP_DEVICE)) + for service in services: + service_id = service.find( + "{0}serviceId".format(URN_UPNP_DEVICE)) - if any([ - service_id is None, - URN_SONY_IRCC not in service_id.text, - ]): - continue + if any([ + service_id is None, + URN_SONY_IRCC not in service_id.text, + ]): + continue - service_location = service.find( - "{0}controlURL".format(URN_UPNP_DEVICE)).text - service_url = lirc_url.scheme + "://" + lirc_url.netloc - self.control_url = service_url + service_location + service_location = service.find( + "{0}controlURL".format(URN_UPNP_DEVICE)).text + service_url = lirc_url.scheme + "://" + lirc_url.netloc + self.control_url = service_url + service_location def _parse_system_information(self, data): - xml_data = xml.etree.ElementTree.fromstring(data) - for element in xml_data.findall("supportFunction"): - for function in element.findall("function"): + for element in find_in_xml( + data, [("supportFunction", "all"), ("function", "all")] + ): + for function in element: if function.attrib["name"] == "WOL": self.mac = function.find( "functionItem").attrib["value"] @@ -225,10 +261,12 @@ def _parse_system_information(self, data): def _parse_dmr(self, data): lirc_url = urlparse(self.ircc_url) xml_data = xml.etree.ElementTree.fromstring(data) - for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): - service_list = device.find( - "{0}serviceList".format(URN_UPNP_DEVICE)) - for service in service_list: + + for device in find_in_xml(xml_data, [ + ("{0}device".format(URN_UPNP_DEVICE), "all"), + "{0}serviceList".format(URN_UPNP_DEVICE) + ]): + for service in device: service_id = service.find( "{0}serviceId".format(URN_UPNP_DEVICE)) if "urn:upnp-org:serviceId:AVTransport" not in service_id.text: @@ -256,6 +294,15 @@ def _parse_dmr(self, data): URN_SCALAR_WEB_API_DEVICE_INFO ) ).text + + search_params = [ + ("{0}device".format(URN_UPNP_DEVICE), "all"), + (device_info_name, "all"), + "{0}X_ScalarWebAPI_BaseURL".format(URN_SCALAR_WEB_API_DEVICE_INFO), + ] + for device in find_in_xml(xml_data, search_params): + for xml_url in device: + base_url = xml_url.text if not base_url.endswith("/"): base_url = "{}/".format(base_url) @@ -298,8 +345,7 @@ def _update_commands(self): json.dumps(resp, indent=4)) def _parse_command_list(self, data): - xml_data = xml.etree.ElementTree.fromstring(data) - for command in xml_data.findall("command"): + for command in find_in_xml(data, [("command", "all")]): name = command.get("name") self.commands[name] = XmlApiObject(command.attrib) @@ -309,9 +355,7 @@ def _update_applist(self): response = self._send_http(url, method=HttpMethod.GET) # todo add support for v4 if response: - xml_data = xml.etree.ElementTree.fromstring(response.text) - apps = xml_data.findall(".//app") - for app in apps: + for app in find_in_xml(response.text, [(".//app", "all")]): data = XmlApiObject({ "name": app.find("name").text, "id": app.find("id").text, @@ -573,9 +617,7 @@ def get_playing_status(self): url=self.av_transport_url, params=data, action=action) if not content: return "OFF" - response = xml.etree.ElementTree.fromstring(content) - state = response.find(".//CurrentTransportState").text - return state + return find_in_xml(content, [".//CurrentTransportState"]).text def get_power_status(self): """Checks if the device is online.""" From 8cc84207e60f06bd57d16d976a8bbb3c271fa625 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 20:06:42 +0200 Subject: [PATCH 084/170] fixed linter issues. --- sonyapilib/device.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 98e2691..a11b610 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -53,13 +53,15 @@ def __init__(self, xml_data): self.type = None self.value = None self.mac = None + # must be named that way to match xml + # pylint: disable=invalid-name self.id = None if not xml_data: return for attr in self.__dict__: if attr == "mode" and xml_data.get(attr): - xml_data[attr] = int(xml_data[attr]) + xml_data[attr] = int(xml_data[attr]) setattr(self, attr, xml_data.get(attr)) class SonyDevice(): From 4d2063063b4cc7aea4cd12bc83cff1602591d155 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 20:34:51 +0200 Subject: [PATCH 085/170] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b8d5526..a6193f5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sonyapilib [![Build Status](https://travis-ci.org/alexmohr/sonyapilib.svg?branch=v4)](https://travis-ci.org/alexmohr/sonyapilib) -[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=v4) +[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.png?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=v4) Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant wich you can find here: From 082534dcff350b234966a5d9b829cb649a30d1a3 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 20:35:02 +0200 Subject: [PATCH 086/170] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a6193f5..b8d5526 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Sonyapilib [![Build Status](https://travis-ci.org/alexmohr/sonyapilib.svg?branch=v4)](https://travis-ci.org/alexmohr/sonyapilib) -[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.png?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=v4) +[![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=v4) Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant wich you can find here: From 9605c65455acc4ba7a7b3f985e7ab85534f6496d Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 22:00:05 +0200 Subject: [PATCH 087/170] Updated readme, added tests. --- README.md | 20 +++-- sonyapilib/device.py | 15 ++-- tests/deviceTest.py | 49 ++++++++--- tests/xml/appsList.xml | 194 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 255 insertions(+), 23 deletions(-) create mode 100644 tests/xml/appsList.xml diff --git a/README.md b/README.md index b8d5526..bf7d468 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,24 @@ [![Build Status](https://travis-ci.org/alexmohr/sonyapilib.svg?branch=v4)](https://travis-ci.org/alexmohr/sonyapilib) [![Coverage Status](https://coveralls.io/repos/github/alexmohr/sonyapilib/badge.svg?branch=v4)](https://coveralls.io/github/alexmohr/sonyapilib?branch=v4) -Sony API lib. This is a python3 conversion from this project https://github.com/KHerron/SonyAPILib and some things have been taken from here: https://github.com/aparraga/braviarc. -It may not contains all functionality which is implemented in the project from KHerron because it is used as base implementation for usage in home assistant wich you can find here: -https://github.com/dilruacs/media_player.sony +This library controls sony devices of all generations which are supported by the TVSideView app. In contrast to other libraries like https://github.com/aparraga/braviarc this supports older generations aswell. + +Code has been taken from the following repositories. +* https://github.com/KHerron/SonyAPILib +* https://github.com/aparraga/braviarc + +This library is used as communication interface in a home assistant component to control media players, which can be found here: https://github.com/dilruacs/media_player.sony + +At the moment not all functions offered by the api are implemented. If you miss a function feel free to create a pull request or open a feature request. + # Installation +To install simply run ``` pip install sonyapilib ``` +This library has been tested with python 3.5 and above, functionality for older python version cannot be guaranteed. + # Example The example will load a json file with all data if it exists or connects to device and registers it, storing the json afterwards ``` @@ -38,14 +48,14 @@ if __name__ == "__main__": pin = input("Enter the PIN displayed at your device: ") device.send_authentication(pin) save_device() - + # wake device is_on = device.get_power_status() if not is_on: device.power(True) apps = device.get_apps() - + device.start_app(apps[0]) # Play media diff --git a/sonyapilib/device.py b/sonyapilib/device.py index a11b610..917bb20 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -46,6 +46,7 @@ class HttpMethod(Enum): class XmlApiObject(): # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" + def __init__(self, xml_data): self.name = None self.mode = None @@ -64,6 +65,7 @@ def __init__(self, xml_data): xml_data[attr] = int(xml_data[attr]) setattr(self, attr, xml_data.get(attr)) + class SonyDevice(): # pylint: disable=too-many-public-methods # pylint: disable=too-many-instance-attributes @@ -301,6 +303,7 @@ def _update_commands(self): self._parse_command_list() def _parse_command_list(self): + """Parse the list of available command in devices with the legacy api.""" url = self._get_action("getRemoteCommandList").url response = self._send_http(url, method=HttpMethod.GET) if not response: @@ -334,12 +337,13 @@ def _recreate_authentication(self): # cookies = None # cookies = requests.cookies.RequestsCookieJar() # cookies.set("auth", self.cookies.get("auth")) + registration_action = self._get_action("register") + if any([not registration_action, registration_action.mode < 3]): + return username = '' - base64string = base64.encodebytes(('%s:%s' % (username, self.pin)) - .encode()).decode().replace('\n', '') - - registration_action = self._get_action("register") + base64string = base64.encodebytes( + ('%s:%s' % (username, self.pin)).encode()).decode().replace('\n', '') self.headers['Authorization'] = "Basic %s" % base64string if registration_action.mode == 3: @@ -469,7 +473,6 @@ def _send_req_ircc(self, params): url=self.control_url, params=data, action=action) return content - def _send_command(self, name): if not self.commands: self._init_device() @@ -515,6 +518,8 @@ def register(self): method=HttpMethod.GET, raise_errors=True) registration_result = AuthenticationResult.SUCCESS + # set the pin to something to make sure init_device is called + self.pin = 9999 except requests.exceptions.HTTPError: registration_result = AuthenticationResult.ERROR diff --git a/tests/deviceTest.py b/tests/deviceTest.py index 2e99a89..9ca2142 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -13,8 +13,9 @@ # cannot be imported at a different position because path modification # is necessary to load the local library. # otherwise it must be installed after every change -from sonyapilib.device import SonyDevice, XmlApiObject, AuthenticationResult +import sonyapilib.device # import to change timeout from sonyapilib.ssdp import SSDPResponse +from sonyapilib.device import SonyDevice, XmlApiObject, AuthenticationResult sys.path.pop(0) @@ -25,6 +26,8 @@ GET_REMOTE_COMMAND_LIST_URL = 'http://192.168.240.4:50002/getRemoteCommandList' REGISTRATION_URL_LEGACY = 'http://192.168.240.4:50002/register' REGISTRATION_URL_V4 = 'http://192.168.178.23/sony/accessControl' +APP_LIST_URL = 'http://test:50202/appslist' + def read_file(file_name): """ Reads a file from disk """ @@ -33,12 +36,15 @@ def read_file(file_name): with open(os.path.join(__location__, file_name)) as f: return f.read() + def mock_error(*args, **kwargs): raise Exception() + def mock_nothing(*args, **kwargs): pass + def mock_discovery(*args, **kwargs): if args[0] == "urn:schemas-sony-com:service:IRCC:1": resp = SSDPResponse(None) @@ -46,6 +52,7 @@ def mock_discovery(*args, **kwargs): return [resp] return None + def mocked_requests_get(*args, **kwargs): class MockResponse: def __init__(self, json_data, status_code, text=None): @@ -59,7 +66,7 @@ def json(self): def raise_for_status(self): pass url = args[0] - print(url) + print("Requesting URL: {}".format(url)) if url == DMR_URL: return MockResponse(None, 200, read_file("xml/dmr_v3.xml")) elif url == IRCC_URL: @@ -70,6 +77,8 @@ def raise_for_status(self): return MockResponse(None, 200, read_file("xml/getSysteminformation.xml")) elif url == GET_REMOTE_COMMAND_LIST_URL: return MockResponse(None, 200, read_file("xml/getRemoteCommandList.xml")) + elif url == APP_LIST_URL: + return MockResponse(None, 200, read_file("xml/appsList.xml")) # elif url == 'http://someotherurl.com/anothertest.json': # return MockResponse({"key2": "value2"}, 200) @@ -145,7 +154,7 @@ def test_update_service_urls_v3(self, mock_ircc, mock_action_list, self.assertEqual(mock_ircc.call_count, 1) self.assertEqual(mock_action_list.call_count, 1) self.assertEqual(mock_system_information.call_count, 1) - + @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_dmr_v3(self, mock_get): content = read_file("xml/dmr_v3.xml") @@ -166,7 +175,7 @@ def test_parse_dmr_v4(self, mock_get): self.assertEqual(device.actions["register"].mode, 4) self.assertEqual( device.actions["getRemoteCommandList"].url, 'http://192.168.178.23/sony/system') - + def test_parse_ircc_error(self): device = self.create_device() device._parse_ircc() @@ -244,30 +253,45 @@ def test_update_commands_v4(self, mock_get): pass @mock.patch('requests.get', side_effect=mocked_requests_get) - def test_update_app_list(self, mock_get): - pass + def test_update_applist(self, mock_get): + device = self.create_device() + app_list = [ + "Video Explorer", "Music Explorer", "Video Player", "Music Player", + "PlayStation Video", "Amazon Prime Video", "Netflix", "Rakuten TV", + "Tagesschau", "Functions with Gracenote ended", "watchmi Themenkanäle", + "Netzkino", "MUBI", "WWE Network", "DW for Smart TV", "YouTube", + "uStudio", "Meteonews TV", "Digital Concert Hall", "Activate Enhanced Features" + ] + + device._update_applist() + for app in device.apps: + self.assertTrue(app in app_list) + self.assertEqual(len(device.apps), len(app_list)) + def test_recreate_authentication_v3(self): device = self.create_device() + device.pin = 1234 self.add_register_to_device(device, 3) device._recreate_authentication() self.assertEqual(device.headers["Authorization"], "Basic OjEyMzQ=") - self.assertEqual(device.headers["X-CERS-DEVICE-ID"], device.get_device_id()) + self.assertEqual( + device.headers["X-CERS-DEVICE-ID"], device.get_device_id()) def test_recreate_authentication_v4(self): device = self.create_device() + device.pin = 1234 self.add_register_to_device(device, 4) device._recreate_authentication() - + self.assertEqual(device.headers["Authorization"], "Basic OjEyMzQ=") self.assertEqual(device.headers["Connection"], "keep-alive") def test_recreate_authentication_v4_psk(self): - # todo implement psk + # todo implement psk pass - @unittest.skip @mock.patch('requests.get', side_effect=mocked_requests_get) def test_register_v1(self, mocked_get): device = self.create_device() @@ -280,13 +304,12 @@ def add_register_to_device(self, device, mode): register_action.mode = mode if mode < 4: register_action.url = REGISTRATION_URL_LEGACY - else: + else: register_action.url = REGISTRATION_URL_V4 device.actions["register"] = register_action - device.pin = 1234 - def create_device(self): + sonyapilib.device.TIMEOUT = 1 return SonyDevice("test", "test") def verify_device_dmr(self, device): diff --git a/tests/xml/appsList.xml b/tests/xml/appsList.xml new file mode 100644 index 0000000..0b10b73 --- /dev/null +++ b/tests/xml/appsList.xml @@ -0,0 +1,194 @@ + + + com.sony.videoexplorer + Video Explorer + + start + + + + + com.sony.musicexplorer + Music Explorer + + start + + + + + com.sony.videoplayer + Video Player + + start + + + + + com.sony.musicplayer + Music Player + + start + + + + + com.sony.videounlimited + PlayStation Video + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_575/x100.png + + + + com.sony.iptv.4976 + Amazon Prime Video + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_456/sub_1/x100.png + + + + com.sony.iptv.type.NRDP + Netflix + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_30/x100.png + + + + com.sony.iptv.3479 + Rakuten TV + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_381/x100.png + + + + com.sony.iptv.type.EU-TAGESSCHAU_6x3 + Tagesschau + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_44/x100.png + + + + com.sony.iptv.6317 + Functions with Gracenote ended + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_684/x100.png + + + + com.sony.iptv.4766 + watchmi Themenkanäle + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_516/x100.png + + + + com.sony.iptv.4742 + Netzkino + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_513/x100.png + + + + com.sony.iptv.5498 + MUBI + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_153/sub_1/x100.png + + + + com.sony.iptv.4340 + WWE Network + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_496/x100.png + + + + com.sony.iptv.4968 + DW for Smart TV + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_551/x100.png + + + + com.sony.iptv.type.ytleanback + YouTube + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_26/sub_1/x100.png + + + + com.sony.iptv.4386 + uStudio + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_109/x100.png + + + + com.sony.iptv.3487 + Meteonews TV + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_382/x100.png + + + + com.sony.iptv.type.WW-BERLINPHIL_NBIV + Digital Concert Hall + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_55/sub_1/x100.png + + + + com.sony.iptv.4834 + Activate Enhanced Features + + start + + +http://static.internet.sony.tv/bivl-ww/static/service/icons/service_365/x100.png + + + \ No newline at end of file From eeb5f84654107e38ed94bddd4bfaa71b41f99ac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Thu, 4 Apr 2019 21:35:28 +0200 Subject: [PATCH 088/170] Move find_xml to new xml_helper.py --- sonyapilib/device.py | 63 +++++++--------------------------------- sonyapilib/xml_helper.py | 40 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 52 deletions(-) create mode 100644 sonyapilib/xml_helper.py diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 2c9b2c0..eefd230 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -18,6 +18,7 @@ import wakeonlan from sonyapilib import ssdp +from sonyapilib.xml_helper import find_in_xml _LOGGER = logging.getLogger(__name__) @@ -30,42 +31,6 @@ WEBAPI_SERVICETYPE = "av:X_ScalarWebAPI_ServiceType" -def xml_search_helper(data, param): - '''Performs find or findall on given xml with string from param.''' - if isinstance(param, (tuple, list)) and param[1] == "all": - result = data.findall(param[0]) - else: - result = data.find(param) - return result - - -def iterate_search_data(data, param): - '''Search in nested lists.''' - result = [] - for element in data: - if isinstance(element, list): - result.append(iterate_search_data(element, param)) - else: - result.append(xml_search_helper(element, param)) - return result - - -def find_in_xml(data, search_params): - '''Takes an xml from string or as xml.etree.ElementTree and an iterable of - strings (or tuple in case of findall) to search.''' - if isinstance(data, str): - data = xml.etree.ElementTree.fromstring(data) - param = search_params[0] - if isinstance(data, list): - result = iterate_search_data(data, param) - else: - result = xml_search_helper(data, param) - - if len(search_params) == 1: - return result - return find_in_xml(result, search_params[1:]) - - class AuthenticationResult(Enum): """Stores the result of the authentication process.""" SUCCESS = 0 @@ -82,6 +47,7 @@ class HttpMethod(Enum): class XmlApiObject(): # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" + def __init__(self, xml_data): self.name = None self.mode = None @@ -100,6 +66,7 @@ def __init__(self, xml_data): xml_data[attr] = int(xml_data[attr]) setattr(self, attr, xml_data.get(attr)) + class SonyDevice(): # pylint: disable=too-many-public-methods # pylint: disable=too-many-instance-attributes @@ -198,7 +165,7 @@ def _parse_action_list(self): if not response: return - for element in find_in_xml(response.text, [("action", "all")]): + for element in find_in_xml(response.text, [("action", True)]): action = XmlApiObject(element.attrib) self.actions[action.name] = action @@ -232,7 +199,7 @@ def _parse_ircc(self): response.text, [upnp_device, "{}serviceList".format(URN_UPNP_DEVICE), - ("{}service".format(URN_UPNP_DEVICE), "all")], + ("{}service".format(URN_UPNP_DEVICE), True)], ) lirc_url = urlparse(self.ircc_url) @@ -258,7 +225,7 @@ def _parse_system_information(self): return for element in find_in_xml( - response.text, [("supportFunction", "all"), ("function", "all")] + response.text, [("supportFunction", "all"), ("function", True)] ): for function in element: if function.attrib["name"] == "WOL": @@ -270,7 +237,7 @@ def _parse_dmr(self, data): xml_data = xml.etree.ElementTree.fromstring(data) for device in find_in_xml(xml_data, [ - ("{0}device".format(URN_UPNP_DEVICE), "all"), + ("{0}device".format(URN_UPNP_DEVICE), True), "{0}serviceList".format(URN_UPNP_DEVICE) ]): for service in device: @@ -294,17 +261,9 @@ def _parse_dmr(self, data): URN_SCALAR_WEB_API_DEVICE_INFO ) - for device in xml_data.findall("{0}device".format(URN_UPNP_DEVICE)): - for device_info in device.findall(device_info_name): - base_url = device_info.find( - "{0}X_ScalarWebAPI_BaseURL".format( - URN_SCALAR_WEB_API_DEVICE_INFO - ) - ).text - search_params = [ - ("{0}device".format(URN_UPNP_DEVICE), "all"), - (device_info_name, "all"), + ("{0}device".format(URN_UPNP_DEVICE), True), + (device_info_name, True), "{0}X_ScalarWebAPI_BaseURL".format(URN_SCALAR_WEB_API_DEVICE_INFO), ] for device in find_in_xml(xml_data, search_params): @@ -354,7 +313,7 @@ def _parse_command_list(self): _LOGGER.error("Failed to get response for command list") return - for command in find_in_xml(response.text, [("command", "all")]): + for command in find_in_xml(response.text, [("command", True)]): name = command.get("name") self.commands[name] = XmlApiObject(command.attrib) @@ -364,7 +323,7 @@ def _update_applist(self): response = self._send_http(url, method=HttpMethod.GET) # todo add support for v4 if response: - for app in find_in_xml(response.text, [(".//app", "all")]): + for app in find_in_xml(response.text, [(".//app", True)]): data = XmlApiObject({ "name": app.find("name").text, "id": app.find("id").text, diff --git a/sonyapilib/xml_helper.py b/sonyapilib/xml_helper.py new file mode 100644 index 0000000..fbe19d2 --- /dev/null +++ b/sonyapilib/xml_helper.py @@ -0,0 +1,40 @@ +"""Some helper functions for the library.""" +import xml.etree.ElementTree + + +def xml_search_helper(data, param): + """Performs find or findall on given xml with string from param.""" + if isinstance(param, (tuple, list)) and param[1]: + result = data.findall(param[0]) + else: + result = data.find(param) + return result + + +def iterate_search_data(data, param): + """Search in nested lists.""" + result = [] + for element in data: + if isinstance(element, list): + result.append(iterate_search_data(element, param)) + else: + result.append(xml_search_helper(element, param)) + return result + + +def find_in_xml(data, search_params): + """Takes an xml from string or as xml.etree.ElementTree and an iterable of + strings (and/or tuples in case of findall) to search. + The tuple should contain the string to search for and a truthy value. + """ + if isinstance(data, str): + data = xml.etree.ElementTree.fromstring(data) + param = search_params[0] + if isinstance(data, list): + result = iterate_search_data(data, param) + else: + result = xml_search_helper(data, param) + + if len(search_params) == 1: + return result + return find_in_xml(result, search_params[1:]) From c4eb8e61e8e040a12070f54ec350566614de1fec Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 22:43:48 +0200 Subject: [PATCH 089/170] Registration error tests. --- sonyapilib/device.py | 103 +++++++++++++++++++++++-------------------- tests/deviceTest.py | 21 ++++++--- 2 files changed, 71 insertions(+), 53 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 917bb20..2f3412b 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -493,12 +493,61 @@ def _get_action(self, name): return self.actions[name] + def _register_without_auth(self, registration_action): + try: + self._send_http( + registration_action.url, + method=HttpMethod.GET, + raise_errors=True) + # set the pin to something to make sure init_device is called + self.pin = 9999 + except requests.exceptions.RequestException: + return AuthenticationResult.ERROR + else: + return AuthenticationResult.SUCCESS + + def _handle_register_error(self, ex): + if isinstance(ex, requests.exceptions.HTTPError) \ + and ex.response.status_code == 401: + return AuthenticationResult.PIN_NEEDED + else: + return AuthenticationResult.ERROR + + def _register_v3(self, registration_action): + try: + self._send_http(registration_action.url, + method=HttpMethod.GET, raise_errors=True) + except requests.exceptions.RequestException as ex: + return self._handle_register_error(ex) + else: + return AuthenticationResult.SUCCESS + + def _register_v4(self, registration_action): + authorization = self._create_api_json("actRegister", 13) + + try: + headers = { + "Content-Type": "application/json" + } + response = self._send_http(registration_action.url, + method=HttpMethod.POST, headers=headers, + data=authorization, raise_errors=True) + + except requests.exceptions.RequestException as ex: + return self._handle_register_error(ex) + else: + resp = response.json() + if resp and not resp.get('error'): + self.cookies = response.cookies + return AuthenticationResult.SUCCESS + else: + return AuthenticationResult.ERROR + def get_device_id(self): """Returns the id which is used for the registration.""" return "TVSideView:{0}".format(self.uuid) def register(self): - # pylint: disable=too-many-branches """ Register at the api. The name which will be displayed in the UI of the device. Make sure this name does not exist yet @@ -510,60 +559,18 @@ def register(self): registration_action = registration_action = self._get_action( "register") - # protocol version 1 and 2 if registration_action.mode < 3: - try: - self._send_http( - registration_action.url, - method=HttpMethod.GET, - raise_errors=True) - registration_result = AuthenticationResult.SUCCESS - # set the pin to something to make sure init_device is called - self.pin = 9999 - except requests.exceptions.HTTPError: - registration_result = AuthenticationResult.ERROR - - # protocol version 3 + registration_result = self._register_without_auth( + registration_action) elif registration_action.mode == 3: - try: - self._send_http(registration_action.url, - method=HttpMethod.GET, raise_errors=True) - except requests.exceptions.HTTPError as ex: - if ex.response.status_code == 401: - registration_result = AuthenticationResult.PIN_NEEDED - else: - registration_result = AuthenticationResult.ERROR - - # newest protocol version 4 this is the same method as braviarc uses + registration_result = self._register_v3(registration_action) elif registration_action.mode == 4: - authorization = self._create_api_json("actRegister", 13) - - try: - headers = { - "Content-Type": "application/json" - } - response = self._send_http(registration_action.url, - method=HttpMethod.POST, headers=headers, - data=authorization, raise_errors=True) - - except requests.exceptions.HTTPError as ex: - _LOGGER.error("[W] HTTPError: %s", str(ex)) - # todo set the correct result. - registration_result = AuthenticationResult.PIN_NEEDED - - except Exception as ex: # pylint: disable=broad-except - _LOGGER.error("[W] Exception: %s", str(ex)) - else: - resp = response.json() - if not resp or not resp.get('error'): - self.cookies = response.cookies - registration_result = AuthenticationResult.SUCCESS - + registration_result = self._register_v4(registration_action) else: raise ValueError( "Regisration mode {0} is not supported".format(registration_action.mode)) - if AuthenticationResult.SUCCESS: + if registration_result is AuthenticationResult.SUCCESS: self._init_device() return registration_result diff --git a/tests/deviceTest.py b/tests/deviceTest.py index 9ca2142..2475c70 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -268,7 +268,6 @@ def test_update_applist(self, mock_get): self.assertTrue(app in app_list) self.assertEqual(len(device.apps), len(app_list)) - def test_recreate_authentication_v3(self): device = self.create_device() device.pin = 1234 @@ -292,12 +291,24 @@ def test_recreate_authentication_v4_psk(self): # todo implement psk pass - @mock.patch('requests.get', side_effect=mocked_requests_get) - def test_register_v1(self, mocked_get): + def register_with_version(self, version): device = self.create_device() - self.add_register_to_device(device, 1) + self.add_register_to_device(device, version) result = device.register() - self.assertEqual(result, AuthenticationResult.SUCCESS) + return result + + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_register_no_auth(self, mocked_get): + versions = [1, 2] + for version in versions: + result = self.register_with_version(version) + self.assertEqual(result, AuthenticationResult.SUCCESS) + + def test_register_fail_http_timeout(self): + versions = [1, 2, 3, 4] + for version in versions: + result = self.register_with_version(version) + self.assertEqual(result, AuthenticationResult.ERROR) def add_register_to_device(self, device, mode): register_action = XmlApiObject({}) From 91eea25877cc64a93469e4c5520ccea29ea77d8f Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 4 Apr 2019 22:51:44 +0200 Subject: [PATCH 090/170] linter and spelling --- sonyapilib/device.py | 14 +++++++------- sonyapilib/xml_helper.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index ba86248..05af57b 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -507,12 +507,12 @@ def _register_without_auth(self, registration_action): else: return AuthenticationResult.SUCCESS - def _handle_register_error(self, ex): + @staticmethod + def _handle_register_error(ex): if isinstance(ex, requests.exceptions.HTTPError) \ and ex.response.status_code == 401: return AuthenticationResult.PIN_NEEDED - else: - return AuthenticationResult.ERROR + return AuthenticationResult.ERROR def _register_v3(self, registration_action): try: @@ -538,12 +538,12 @@ def _register_v4(self, registration_action): return self._handle_register_error(ex) else: resp = response.json() - if resp and not resp.get('error'): - self.cookies = response.cookies - return AuthenticationResult.SUCCESS - else: + if not resp or resp.get('error'): return AuthenticationResult.ERROR + self.cookies = response.cookies + return AuthenticationResult.SUCCESS + def get_device_id(self): """Returns the id which is used for the registration.""" return "TVSideView:{0}".format(self.uuid) diff --git a/sonyapilib/xml_helper.py b/sonyapilib/xml_helper.py index fbe19d2..9c5ed88 100644 --- a/sonyapilib/xml_helper.py +++ b/sonyapilib/xml_helper.py @@ -1,4 +1,4 @@ -"""Some helper functions for the library.""" +"""XML helper functions for the library.""" import xml.etree.ElementTree @@ -25,7 +25,7 @@ def iterate_search_data(data, param): def find_in_xml(data, search_params): """Takes an xml from string or as xml.etree.ElementTree and an iterable of strings (and/or tuples in case of findall) to search. - The tuple should contain the string to search for and a truthy value. + The tuple should contain the string to search for and a true value. """ if isinstance(data, str): data = xml.etree.ElementTree.fromstring(data) From c84683b9da04c79cceac547b0ee58c884361d82d Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 5 Apr 2019 09:17:25 +0200 Subject: [PATCH 091/170] Fixed #28 --- sonyapilib/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 05af57b..24c1af4 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -620,7 +620,7 @@ def get_power_status(self): # todo parse response self._send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) - except requests.exceptions.HTTPError as ex: + except requests.exceptions.RequestException as ex: _LOGGER.debug(ex) return False return True From f88a6a66a8615e44ded89781084f2be90f7278eb Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 5 Apr 2019 09:26:50 +0200 Subject: [PATCH 092/170] changed authentication --- sonyapilib/device.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 24c1af4..17268a1 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -588,11 +588,7 @@ def send_authentication(self, pin): self._recreate_authentication() result = self.register() - if AuthenticationResult.SUCCESS == result: - self._init_device() - return True - - return False + return AuthenticationResult.SUCCESS == result def wakeonlan(self, broadcast='255.255.255.255'): """Starts the device either via wakeonlan.""" From 1e79bd20b79eded38a50705eae030accee00b3fe Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 5 Apr 2019 10:37:17 +0200 Subject: [PATCH 093/170] extended registration tests. --- tests/deviceTest.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/deviceTest.py b/tests/deviceTest.py index 2475c70..bc7f9f7 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -79,6 +79,8 @@ def raise_for_status(self): return MockResponse(None, 200, read_file("xml/getRemoteCommandList.xml")) elif url == APP_LIST_URL: return MockResponse(None, 200, read_file("xml/appsList.xml")) + elif url == REGISTRATION_URL_LEGACY: + return MockResponse({}, 200) # elif url == 'http://someotherurl.com/anothertest.json': # return MockResponse({"key2": "value2"}, 200) @@ -304,11 +306,27 @@ def test_register_no_auth(self, mocked_get): result = self.register_with_version(version) self.assertEqual(result, AuthenticationResult.SUCCESS) - def test_register_fail_http_timeout(self): + @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_register_not_supported(self, mocked_get, mocked_init_device): + with self.assertRaises(ValueError): + self.register_with_version(5) + self.assertEqual(mocked_init_device.call_count, 0) + + @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + def test_register_fail_http_timeout(self, mocked_init_device): versions = [1, 2, 3, 4] for version in versions: result = self.register_with_version(version) self.assertEqual(result, AuthenticationResult.ERROR) + self.assertEqual(mocked_init_device.call_count, 0) + + @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_register_success_v3(self, mocked_requests_get, mocked_init_device): + result = self.register_with_version(3) + self.assertEqual(result, AuthenticationResult.SUCCESS) + self.assertEqual(mocked_init_device.call_count, 1) def add_register_to_device(self, device, mode): register_action = XmlApiObject({}) From dd8cd784d4c215f97c063d5bddce757f99a6b85a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Fri, 5 Apr 2019 10:33:44 +0200 Subject: [PATCH 094/170] Refactor _send_http --- sonyapilib/device.py | 63 ++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 40 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 17268a1..424d5cd 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -38,12 +38,6 @@ class AuthenticationResult(Enum): PIN_NEEDED = 2 -class HttpMethod(Enum): - """Defines which http method is used.""" - GET = 0 - POST = 1 - - class XmlApiObject(): # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" @@ -141,7 +135,7 @@ def save_to_json(self): def _update_service_urls(self): """Initialize the device by reading the necessary resources from it.""" - response = self._send_http(self.dmr_url, method=HttpMethod.GET) + response = self._send_http(self.dmr_url, method="get") if not response: _LOGGER.error("Failed to get DMR") return @@ -161,7 +155,7 @@ def _update_service_urls(self): def _parse_action_list(self): response = self._send_http( - self.actionlist_url, method=HttpMethod.GET) + self.actionlist_url, method="get") if not response: return @@ -183,7 +177,7 @@ def _parse_action_list(self): def _parse_ircc(self): response = self._send_http( - self.ircc_url, method=HttpMethod.GET) + self.ircc_url, method="get") if not response: return @@ -220,7 +214,7 @@ def _parse_ircc(self): def _parse_system_information(self): response = self._send_http( - self._get_action("getSystemInformation").url, method=HttpMethod.GET) + self._get_action("getSystemInformation").url, method="get") if not response: return @@ -309,7 +303,7 @@ def _update_commands(self): def _parse_command_list(self): """Parse the list of available command in devices with the legacy api.""" url = self._get_action("getRemoteCommandList").url - response = self._send_http(url, method=HttpMethod.GET) + response = self._send_http(url, method="get") if not response: _LOGGER.error("Failed to get response for command list") return @@ -321,7 +315,7 @@ def _parse_command_list(self): def _update_applist(self): """Update the list of apps which are supported by the device.""" url = self.app_url + "/appslist" - response = self._send_http(url, method=HttpMethod.GET) + response = self._send_http(url, method="get") # todo add support for v4 if response: for app in find_in_xml(response.text, [(".//app", True)]): @@ -403,37 +397,26 @@ def _create_api_json(self, method, params=None): return ret - def _send_http(self, url, method, data=None, headers=None, log_errors=True, raise_errors=False): + def _send_http(self, url, method, **kwargs): # pylint: disable=too-many-arguments """Send request command via HTTP json to Sony Bravia.""" - if not headers: - headers = self.headers + log_errors = kwargs.pop("log_errors", True) + raise_errors = kwargs.pop("raise_errors", False) + method = kwargs.pop("method", method) - if not url: - return None + standard_params = { + "cookies": self.cookies, + "timeout": TIMEOUT, + "headers": self.headers, + } + kwargs.update(standard_params) _LOGGER.debug( "Calling http url %s method %s", url, method) try: - params = "" - if data: - params = data.encode("UTF-8") - - if method == HttpMethod.POST: - response = requests.post(url, - data=params, - headers=headers, - cookies=self.cookies, - timeout=TIMEOUT) - elif method == HttpMethod.GET: - response = requests.get(url, - data=params, - headers=headers, - cookies=self.cookies, - timeout=TIMEOUT) - + response = getattr(requests, method)(url, kwargs) response.raise_for_status() except requests.exceptions.RequestException as ex: if log_errors: @@ -457,7 +440,7 @@ def _post_soap_request(self, url, params, action): """.format(params) response = self._send_http( - url, method=HttpMethod.POST, headers=headers, data=data) + url, method="post", headers=headers, data=data) if response: return response.content.decode("utf-8") return False @@ -498,7 +481,7 @@ def _register_without_auth(self, registration_action): try: self._send_http( registration_action.url, - method=HttpMethod.GET, + method="get", raise_errors=True) # set the pin to something to make sure init_device is called self.pin = 9999 @@ -517,7 +500,7 @@ def _handle_register_error(ex): def _register_v3(self, registration_action): try: self._send_http(registration_action.url, - method=HttpMethod.GET, raise_errors=True) + method="get", raise_errors=True) except requests.exceptions.RequestException as ex: return self._handle_register_error(ex) else: @@ -531,7 +514,7 @@ def _register_v4(self, registration_action): "Content-Type": "application/json" } response = self._send_http(registration_action.url, - method=HttpMethod.POST, headers=headers, + method="post", headers=headers, data=authorization, raise_errors=True) except requests.exceptions.RequestException as ex: @@ -614,7 +597,7 @@ def get_power_status(self): url = self.actionlist_url try: # todo parse response - self._send_http(url, HttpMethod.GET, + self._send_http(url, "get", log_errors=False, raise_errors=True) except requests.exceptions.RequestException as ex: _LOGGER.debug(ex) @@ -628,7 +611,7 @@ def start_app(self, app_name): self.home() url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) data = "LOCATION: {0}/run".format(url) - self._send_http(url, HttpMethod.POST, data=data) + self._send_http(url, "post", data=data) def power(self, power_on, broadcast=None): """Powers the device on or shuts it off.""" From 6e6629f255a589bbd0a1ed7ddbb46fb88105b4a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Fri, 5 Apr 2019 15:29:57 +0200 Subject: [PATCH 095/170] Remove _request_json method This method is no longer necessary after refactoring _send_http. Also removing json-duming from _create_api_json. --- sonyapilib/device.py | 59 +++++++++++--------------------------------- 1 file changed, 14 insertions(+), 45 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 424d5cd..5a1b173 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -154,8 +154,7 @@ def _update_service_urls(self): _LOGGER.error("failed to get device information: %s", str(ex)) def _parse_action_list(self): - response = self._send_http( - self.actionlist_url, method="get") + response = self._send_http(self.actionlist_url, method="get") if not response: return @@ -176,8 +175,7 @@ def _parse_action_list(self): action.url = action.url + "&wolSupport=true" def _parse_ircc(self): - response = self._send_http( - self.ircc_url, method="get") + response = self._send_http(self.ircc_url, method="get") if not response: return @@ -290,7 +288,9 @@ def _update_commands(self): action = self.actions[action_name] json_data = self._create_api_json(action.value) - resp = self._request_json(action.url, json_data, None) + resp = self._send_http( + action.url, "post", json=json_data, headers={} + ).json() if resp and not resp.get('error'): # todo parse this into the old structure. self.commands = resp.get('result')[1] @@ -346,33 +346,6 @@ def _recreate_authentication(self): elif registration_action.mode == 4: self.headers['Connection'] = "keep-alive" - def _request_json(self, url, params, log_errors=True): - """Send request command via HTTP json to Sony Bravia.""" - - headers = {} - - built_url = 'http://{}/{}'.format(self.host, url) - - try: - # todo refactor to use http send. - response = requests.post(built_url, - data=params.encode("UTF-8"), - cookies=self.cookies, - timeout=TIMEOUT, - headers=headers) - - except requests.exceptions.HTTPError as exception_instance: - if log_errors: - _LOGGER.error("HTTPError: %s", str(exception_instance)) - - except Exception as exception_instance: # pylint: disable=broad-except - if log_errors: - _LOGGER.error("Exception: %s", str(exception_instance)) - - else: - html = json.loads(response.content.decode('utf-8')) - return html - def _create_api_json(self, method, params=None): # pylint: disable=invalid-name """Create json data which will be send via post for the V4 api""" @@ -387,15 +360,12 @@ def _create_api_json(self, method, params=None): "function": "WOL" }]] - ret = json.dumps( - { - "method": method, - "params": params, - "id": 1, - "version": "1.0" - }) - - return ret + return { + "method": method, + "params": params, + "id": 1, + "version": "1.0" + } def _send_http(self, url, method, **kwargs): # pylint: disable=too-many-arguments @@ -416,7 +386,7 @@ def _send_http(self, url, method, **kwargs): "Calling http url %s method %s", url, method) try: - response = getattr(requests, method)(url, kwargs) + response = getattr(requests, method)(url, **kwargs) response.raise_for_status() except requests.exceptions.RequestException as ex: if log_errors: @@ -515,7 +485,7 @@ def _register_v4(self, registration_action): } response = self._send_http(registration_action.url, method="post", headers=headers, - data=authorization, raise_errors=True) + json=authorization, raise_errors=True) except requests.exceptions.RequestException as ex: return self._handle_register_error(ex) @@ -597,8 +567,7 @@ def get_power_status(self): url = self.actionlist_url try: # todo parse response - self._send_http(url, "get", - log_errors=False, raise_errors=True) + self._send_http(url, "get", log_errors=False, raise_errors=True) except requests.exceptions.RequestException as ex: _LOGGER.debug(ex) return False From 35d3469148ef6eec1a34f9a4429f89ead0b6e0cf Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 5 Apr 2019 20:42:41 +0200 Subject: [PATCH 096/170] continued tests. --- .gitignore | 5 ++- sonyapilib/device.py | 2 -- tests/deviceTest.py | 78 +++++++++++++++++++++++++++++--------------- 3 files changed, 56 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 89ce0e4..2a67c01 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,8 @@ __pycache__/ *.so + + # Distribution / packaging .Python build/ @@ -108,4 +110,5 @@ venv.bak/ # mypy .mypy_cache/ -.coveralls \ No newline at end of file +.coveralls +.idea/ \ No newline at end of file diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 17268a1..d51f4ad 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -37,13 +37,11 @@ class AuthenticationResult(Enum): ERROR = 1 PIN_NEEDED = 2 - class HttpMethod(Enum): """Defines which http method is used.""" GET = 0 POST = 1 - class XmlApiObject(): # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" diff --git a/tests/deviceTest.py b/tests/deviceTest.py index bc7f9f7..762f0dd 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -26,6 +26,7 @@ GET_REMOTE_COMMAND_LIST_URL = 'http://192.168.240.4:50002/getRemoteCommandList' REGISTRATION_URL_LEGACY = 'http://192.168.240.4:50002/register' REGISTRATION_URL_V4 = 'http://192.168.178.23/sony/accessControl' +REGISTRATION_URL_V4_FAIL = 'http://192.168.178.22/sony/accessControl' APP_LIST_URL = 'http://test:50202/appslist' @@ -36,15 +37,12 @@ def read_file(file_name): with open(os.path.join(__location__, file_name)) as f: return f.read() - def mock_error(*args, **kwargs): raise Exception() - def mock_nothing(*args, **kwargs): pass - def mock_discovery(*args, **kwargs): if args[0] == "urn:schemas-sony-com:service:IRCC:1": resp = SSDPResponse(None) @@ -52,21 +50,42 @@ def mock_discovery(*args, **kwargs): return [resp] return None +class MockResponse(): + class MockResponseJson: + def __init__(self, data): + self.data = data + + def get(self, key): + if key in self.data: + return self.data[key] + return None + + def __init__(self, json_data, status_code, text=None, cookies=None): + self.json_obj = self.MockResponseJson(json_data) + self.status_code = status_code + self.text = text + self.cookies = cookies + + def json(self): + return self.json_obj + + def get(self): + pass -def mocked_requests_get(*args, **kwargs): - class MockResponse: - def __init__(self, json_data, status_code, text=None): - self.json_data = json_data - self.status_code = status_code - self.text = text + def raise_for_status(self): + pass - def json(self): - return self.json_data +def mocked_requests_post(*args, **kwargs): + url = args[0] + print("POST for URL: {}".format(url)) + if url == REGISTRATION_URL_V4: + return MockResponse({}, 200) + elif url == REGISTRATION_URL_V4_FAIL: + return MockResponse({"error": 402}, 200) - def raise_for_status(self): - pass +def mocked_requests_get(*args, **kwargs): url = args[0] - print("Requesting URL: {}".format(url)) + print("GET for URL: {}".format(url)) if url == DMR_URL: return MockResponse(None, 200, read_file("xml/dmr_v3.xml")) elif url == IRCC_URL: @@ -81,12 +100,9 @@ def raise_for_status(self): return MockResponse(None, 200, read_file("xml/appsList.xml")) elif url == REGISTRATION_URL_LEGACY: return MockResponse({}, 200) - # elif url == 'http://someotherurl.com/anothertest.json': - # return MockResponse({"key2": "value2"}, 200) return MockResponse(None, 404) - class SonyDeviceTest(unittest.TestCase): @mock.patch('sonyapilib.device.SonyDevice._update_service_urls', side_effect=mock_nothing) @@ -173,7 +189,7 @@ def test_parse_dmr_v4(self, mock_get): self.verify_device_dmr(device) self.assertTrue(device.is_v4) self.assertEqual( - device.actions["register"].url, 'http://192.168.178.23/sony/accessControl') + device.actions["register"].url, REGISTRATION_URL_V4) self.assertEqual(device.actions["register"].mode, 4) self.assertEqual( device.actions["getRemoteCommandList"].url, 'http://192.168.178.23/sony/system') @@ -187,7 +203,7 @@ def test_parse_ircc(self, mock_get): device = self.create_device() device._parse_ircc() self.assertEqual( - device.actionlist_url, 'http://192.168.240.4:50002/actionList') + device.actionlist_url, ACTION_LIST_URL) self.assertEqual( device.control_url, 'http://test:50001/upnp/control/IRCC') @@ -252,6 +268,7 @@ def test_update_commands_v3(self, mock_parse_cmd_list): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_update_commands_v4(self, mock_get): + # todo implement parsing of command list pass @mock.patch('requests.get', side_effect=mocked_requests_get) @@ -293,12 +310,6 @@ def test_recreate_authentication_v4_psk(self): # todo implement psk pass - def register_with_version(self, version): - device = self.create_device() - self.add_register_to_device(device, version) - result = device.register() - return result - @mock.patch('requests.get', side_effect=mocked_requests_get) def test_register_no_auth(self, mocked_get): versions = [1, 2] @@ -328,6 +339,13 @@ def test_register_success_v3(self, mocked_requests_get, mocked_init_device): self.assertEqual(result, AuthenticationResult.SUCCESS) self.assertEqual(mocked_init_device.call_count, 1) + @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_register_no_json_v4(self, mocked_requests_post, mocked_init_device): + result = self.register_with_version(4, REGISTRATION_URL_V4_FAIL) + self.assertEqual(result, AuthenticationResult.ERROR) + self.assertEqual(mocked_init_device.call_count, 0) + def add_register_to_device(self, device, mode): register_action = XmlApiObject({}) register_action.mode = mode @@ -337,6 +355,15 @@ def add_register_to_device(self, device, mode): register_action.url = REGISTRATION_URL_V4 device.actions["register"] = register_action + def register_with_version(self, version, reg_url=""): + device = self.create_device() + self.add_register_to_device(device, version) + if reg_url: + device.actions["register"].url = reg_url + + result = device.register() + return result + def create_device(self): sonyapilib.device.TIMEOUT = 1 return SonyDevice("test", "test") @@ -345,6 +372,5 @@ def verify_device_dmr(self, device): self.assertEqual(device.av_transport_url, 'http://test:52323/upnp/control/AVTransport') - if __name__ == '__main__': unittest.main() From 118293e07f679eaf93d15b0933cc80ca216f551b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 5 Apr 2019 22:56:18 +0200 Subject: [PATCH 097/170] added testcase to make sure uuid is the same. --- tests/deviceTest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/deviceTest.py b/tests/deviceTest.py index 762f0dd..ae7a886 100644 --- a/tests/deviceTest.py +++ b/tests/deviceTest.py @@ -144,6 +144,7 @@ def test_save_load_from_json(self): restored_device = SonyDevice.load_from_json(jdata) jdata_restored = restored_device.save_to_json() self.assertEqual(jdata, jdata_restored) + self.assertEqual(restored_device.uuid, device.uuid) def test_update_service_urls_error_response(self): device = self.create_device() From 66a3710bd5020dc0cea24311bc14b3d5aaddcf8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20Sch=C3=B6nthaler?= Date: Sat, 6 Apr 2019 21:35:52 +0200 Subject: [PATCH 098/170] Fix params for http request Before the params cookies, timeout and headers couldn't be overwritten with keyword-args. --- sonyapilib/device.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index fd91e6a..d8633b8 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -379,18 +379,18 @@ def _send_http(self, url, method, **kwargs): raise_errors = kwargs.pop("raise_errors", False) method = kwargs.pop("method", method.value) - standard_params = { + params = { "cookies": self.cookies, "timeout": TIMEOUT, "headers": self.headers, } - kwargs.update(standard_params) + params.update(kwargs) _LOGGER.debug( "Calling http url %s method %s", url, method) try: - response = getattr(requests, method)(url, **kwargs) + response = getattr(requests, method)(url, **params) response.raise_for_status() except requests.exceptions.RequestException as ex: if log_errors: From 96cf214e37f34047a037c1275f56df04cc5dfd4c Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 6 Apr 2019 23:08:49 +0200 Subject: [PATCH 099/170] Added ssdp test, refactored structure, running tests via setup.py, removed .vscode from repository --- .gitignore | 3 +- .travis.yml | 5 +- .vscode/launch.json | 22 - {tests => examples}/simple_example.py | 8 +- setup.py | 14 +- sonyapilib/device.py | 15 +- sonyapilib/ssdp.py | 35 +- test_requirements.txt | 6 - tests/__init__.py | 0 tests/{xml => data}/actionlist.xml | 0 tests/{xml => data}/appsList.xml | 0 tests/{xml => data}/dmr_v3.xml | 0 tests/{xml => data}/dmr_v4.xml | 0 tests/{xml => data}/getRemoteCommandList.xml | 0 tests/{xml => data}/getSysteminformation.xml | 0 tests/{xml => data}/ircc.xml | 0 tests/data/ssdp.txt | 981 +++++++++++++++++++ tests/{deviceTest.py => device_test.py} | 42 +- tests/ssdp_test.py | 66 ++ tests/testutil.py | 18 + 20 files changed, 1131 insertions(+), 84 deletions(-) delete mode 100644 .vscode/launch.json rename {tests => examples}/simple_example.py (95%) delete mode 100644 test_requirements.txt create mode 100644 tests/__init__.py rename tests/{xml => data}/actionlist.xml (100%) rename tests/{xml => data}/appsList.xml (100%) rename tests/{xml => data}/dmr_v3.xml (100%) rename tests/{xml => data}/dmr_v4.xml (100%) rename tests/{xml => data}/getRemoteCommandList.xml (100%) rename tests/{xml => data}/getSysteminformation.xml (100%) rename tests/{xml => data}/ircc.xml (100%) create mode 100644 tests/data/ssdp.txt rename tests/{deviceTest.py => device_test.py} (93%) create mode 100644 tests/ssdp_test.py create mode 100644 tests/testutil.py diff --git a/.gitignore b/.gitignore index 2a67c01..834e0ed 100644 --- a/.gitignore +++ b/.gitignore @@ -111,4 +111,5 @@ venv.bak/ .mypy_cache/ .coveralls -.idea/ \ No newline at end of file +.idea/ +.vscode/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 608b620..9f10993 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,7 @@ language: python python: - "3.5" cache: pip -install: - - pip install -r test_requirements.txt - - pip install . script: - - py.test tests/*Test.py --cov=sonyapilib + - coverage run setup.py test - pylint sonyapilib - coveralls \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 35bc23c..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - "stopOnEntry": false, - "pythonPath": "${config:python.pythonPath}", - "program": "${file}", - "cwd": "${workspaceFolder}", - "env": {}, - "envFile": "${workspaceFolder}/.env", - "debugOptions": [ - "RedirectOutput" - ] - } - ] -} \ No newline at end of file diff --git a/tests/simple_example.py b/examples/simple_example.py similarity index 95% rename from tests/simple_example.py rename to examples/simple_example.py index 1bfd817..7b0a799 100644 --- a/tests/simple_example.py +++ b/examples/simple_example.py @@ -1,9 +1,7 @@ -import unittest -import os.path - -from inspect import getsourcefile import os.path as path import sys +from inspect import getsourcefile + current_dir = path.dirname(path.abspath(getsourcefile(lambda: 0))) sys.path.insert(0, current_dir[:current_dir.rfind(path.sep)]) from sonyapilib.device import SonyDevice, AuthenticationResult @@ -20,10 +18,10 @@ def register_device(device): device.send_authentication(pin) return True + if __name__ == "__main__": stored_config = "bluray.json" - device = None # device must be on for registration host = "10.0.0.102" diff --git a/setup.py b/setup.py index 102226c..43ef3a1 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,11 @@ # pylint: disable=invalid-name, exec-used """Setup sonyapilib package.""" from __future__ import absolute_import -import sys + import os -from setuptools import setup +import sys +from setuptools import setup sys.path.insert(0, '.') @@ -31,6 +32,13 @@ 'jsonpickle', 'setuptools', 'requests', - 'wakeonlan', + 'wakeonlan' ], + tests_require=['pytest>=3.6', + 'pytest-pep8', + 'pytest-cov', + 'python-coveralls', + 'pylint', + 'coverage>=4.4' + ] ) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index fd91e6a..7680452 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -1,5 +1,5 @@ """ -Sony Mediaplayer lib +Sony Media player lib """ from enum import Enum from urllib.parse import ( @@ -37,12 +37,14 @@ class AuthenticationResult(Enum): ERROR = 1 PIN_NEEDED = 2 + class HttpMethod(Enum): """Defines which http method is used.""" GET = "get" POST = "post" -class XmlApiObject(): + +class XmlApiObject: # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" @@ -65,7 +67,7 @@ def __init__(self, xml_data): setattr(self, attr, xml_data.get(attr)) -class SonyDevice(): +class SonyDevice: # pylint: disable=too-many-public-methods # pylint: disable=too-many-instance-attributes # pylint: disable=fixme @@ -508,12 +510,11 @@ def get_device_id(self): def register(self): """ Register at the api. The name which will be displayed in the UI of the device. - Make sure this name does not exist yet + Make sure this name does not exist yet. For this the device must be put in registration mode. - The tested sd5500 has no separate mode but allows registration in the overview """ - registration_result = AuthenticationResult.ERROR + registration_result = AuthenticationResult.ERROR registration_action = registration_action = self._get_action( "register") @@ -526,7 +527,7 @@ def register(self): registration_result = self._register_v4(registration_action) else: raise ValueError( - "Regisration mode {0} is not supported".format(registration_action.mode)) + "Registration mode {0} is not supported".format(registration_action.mode)) if registration_result is AuthenticationResult.SUCCESS: self._init_device() diff --git a/sonyapilib/ssdp.py b/sonyapilib/ssdp.py index d0dcd07..b68ec03 100644 --- a/sonyapilib/ssdp.py +++ b/sonyapilib/ssdp.py @@ -1,25 +1,24 @@ """ SSDP Implementation - """ - +import email +import logging import socket from io import StringIO -import email -class SSDPResponse(): +_LOGGER = logging.getLogger(__name__) + + +class SSDPResponse: # pylint: disable=too-few-public-methods """Holds the response of a ssdp request.""" def __init__(self, response): if not response: return - # pop the first line so we only process headers - # first line is http response - _, headers = response.split('\r\n', 1) - + # construct a message from the request string - message = email.message_from_file(StringIO(headers)) + message = email.message_from_file(StringIO(response)) # construct a dictionary containing the headers headers = dict(message.items()) @@ -59,17 +58,29 @@ def discover(service="ssdp:all", timeout=1, retries=5, mx=3): sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) - #msg = DISCOVERY_MSG % dict(service='id1', library=LIB_ID) + # msg = DISCOVERY_MSG % dict(service='id1', library=LIB_ID) for _ in range(0, retries): # sending it more than once will # decrease the probability of a timeout sock.sendto(str.encode(message.format( *host, st=service, mx=mx)), host) + data = "" while True: try: - response = SSDPResponse(bytes.decode(sock.recv(1024))) - responses[response.location] = response + data = data + bytes.decode(sock.recv(1024)) except socket.timeout: break + + lines = "" + http_ok = "HTTP/1.1 200 OK" + for line in data.split('\r\n'): + if http_ok in line and len(lines) > 0: + response = SSDPResponse(lines) + responses[response.location] = response + lines = "" + elif http_ok not in line: + line_content = line.split(":") + if len(line_content) >= 2 and line_content[1]: + lines += line + '\r\n' return list(responses.values()) diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index 77e462b..0000000 --- a/test_requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -pytest>=3.6 -pytest-pep8 -pytest-cov -python-coveralls -pylint -git+https://github.com/melor/HTTPretty.git@py33 \ No newline at end of file diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/xml/actionlist.xml b/tests/data/actionlist.xml similarity index 100% rename from tests/xml/actionlist.xml rename to tests/data/actionlist.xml diff --git a/tests/xml/appsList.xml b/tests/data/appsList.xml similarity index 100% rename from tests/xml/appsList.xml rename to tests/data/appsList.xml diff --git a/tests/xml/dmr_v3.xml b/tests/data/dmr_v3.xml similarity index 100% rename from tests/xml/dmr_v3.xml rename to tests/data/dmr_v3.xml diff --git a/tests/xml/dmr_v4.xml b/tests/data/dmr_v4.xml similarity index 100% rename from tests/xml/dmr_v4.xml rename to tests/data/dmr_v4.xml diff --git a/tests/xml/getRemoteCommandList.xml b/tests/data/getRemoteCommandList.xml similarity index 100% rename from tests/xml/getRemoteCommandList.xml rename to tests/data/getRemoteCommandList.xml diff --git a/tests/xml/getSysteminformation.xml b/tests/data/getSysteminformation.xml similarity index 100% rename from tests/xml/getSysteminformation.xml rename to tests/data/getSysteminformation.xml diff --git a/tests/xml/ircc.xml b/tests/data/ircc.xml similarity index 100% rename from tests/xml/ircc.xml rename to tests/data/ircc.xml diff --git a/tests/data/ssdp.txt b/tests/data/ssdp.txt new file mode 100644 index 0000000..ad1a88e --- /dev/null +++ b/tests/data/ssdp.txt @@ -0,0 +1,981 @@ +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: uuid:75802409-bccb-40e7-9f6c-5C49796832C5 +USN: uuid:75802409-bccb-40e7-9f6c-5C49796832C5 +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:device:InternetGatewayDevice:2 +USN: uuid:75802409-bccb-40e7-9f6c-5C49796832C5::urn:schemas-upnp-org:device:InternetGatewayDevice:2 + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: uuid:75802409-bccb-40e7-9f6b-5C49796832C5 +USN: uuid:75802409-bccb-40e7-9f6b-5C49796832C5 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:device:WANDevice:2 +USN: uuid:75802409-bccb-40e7-9f6b-5C49796832C5::urn:schemas-upnp-org:device:WANDevice:2 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: uuid:75802409-bccb-40e7-9f6a-5C49796832C5 +USN: uuid:75802409-bccb-40e7-9f6a-5C49796832C5 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:device:WANConnectionDevice:2 +USN: uuid:75802409-bccb-40e7-9f6a-5C49796832C5::urn:schemas-upnp-org:device:WANConnectionDevice:2 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/fboxdesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: upnp:rootdevice +USN: uuid:123402409-bccb-40e7-8e6c-5C49796832C5::upnp:rootdevice + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/fboxdesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: uuid:123402409-bccb-40e7-8e6c-5C49796832C5 +USN: uuid:123402409-bccb-40e7-8e6c-5C49796832C5 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/fboxdesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:device:fritzbox:1 +USN: uuid:123402409-bccb-40e7-8e6c-5C49796832C5::urn:schemas-upnp-org:device:fritzbox:1 +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igddesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: upnp:rootdevice +USN: uuid:75802409-bccb-40e7-8e6c-5C49796832C5::upnp:rootdevice +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igddesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: uuid:75802409-bccb-40e7-8e6c-5C49796832C5 +USN: uuid:75802409-bccb-40e7-8e6c-5C49796832C5 +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igddesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:device:InternetGatewayDevice:1 +USN: uuid:75802409-bccb-40e7-8e6c-5C49796832C5::urn:schemas-upnp-org:device:InternetGatewayDevice:1 +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igddesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: uuid:76802409-bccb-40e7-8e6b-5C49796832C5 +USN: uuid:76802409-bccb-40e7-8e6b-5C49796832C5 +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igddesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:device:WANDevice:1 +USN: uuid:76802409-bccb-40e7-8e6b-5C49796832C5::urn:schemas-upnp-org:device:WANDevice:1 +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igddesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: uuid:76802409-bccb-40e7-8e6a-5C49796832C5 +USN: uuid:76802409-bccb-40e7-8e6a-5C49796832C5 +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igddesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:device:WANConnectionDevice:1 +USN: uuid:76802409-bccb-40e7-8e6a-5C49796832C5::urn:schemas-upnp-org:device:WANConnectionDevice:1 +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/avmnexusdesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: upnp:rootdevice +USN: uuid:535502409-bccb-40e7-8e6c-5C49796832C5::upnp:rootdevice +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/avmnexusdesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: uuid:535502409-bccb-40e7-8e6c-5C49796832C5 +USN: uuid:535502409-bccb-40e7-8e6c-5C49796832C5 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/avmnexusdesc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:device:avmnexus:1 +USN: uuid:535502409-bccb-40e7-8e6c-5C49796832C5::urn:schemas-upnp-org:device:avmnexus:1 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/l2tpv3.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-any-com:service:l2tpv3:1 +USN: uuid:95802409-bccb-40e7-8e6c-5C49796832C5::urn:schemas-any-com:service:l2tpv3:1 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-any-com:service:Any:1 +USN: uuid:75802409-bccb-40e7-9f6c-5C49796832C5::urn:schemas-any-com:service:Any:1 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 +USN: uuid:75802409-bccb-40e7-9f6b-5C49796832C5::urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:service:WANDSLLinkConfig:1 +USN: uuid:75802409-bccb-40e7-9f6a-5C49796832C5::urn:schemas-upnp-org:service:WANDSLLinkConfig:1 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:service:WANIPConnection:2 +USN: uuid:75802409-bccb-40e7-9f6a-5C49796832C5::urn:schemas-upnp-org:service:WANIPConnection:2 + + +HTTP/1.1 200 OK +LOCATION: http://10.0.0.1:49000/igd2desc.xml +SERVER: core UPnP/1.0 AVM FRITZ!Box 7490 113.07.01 +CACHE-CONTROL: max-age=1800 +EXT: +ST: urn:schemas-upnp-org:service:WANIPv6FirewallControl:1 +USN: uuid:75802409-bccb-40e7-9f6a-5C49796832C5::urn:schemas-upnp-org:service:WANIPv6FirewallControl:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: upnp:rootdevice +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::upnp:rootdevice +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:device:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:device:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:service:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: upnp:rootdevice +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: upnp:rootdevice +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::upnp:rootdevice +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:device:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:device:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:service:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: uuid:00000004-0000-1010-8000-3052cbcc16ee +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:schemas-upnp-org:device:Basic:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:schemas-upnp-org:device:Basic:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: upnp:rootdevice +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: upnp:rootdevice +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: upnp:rootdevice +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: upnp:rootdevice +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: upnp:rootdevice +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:dial-multiscreen-org:service:dial:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: upnp:rootdevice +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: uuid:00000004-0000-1010-8000-3052cbcc16ee +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:schemas-upnp-org:device:Basic:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:schemas-upnp-org:device:Basic:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:dial-multiscreen-org:service:dial:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: upnp:rootdevice +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: uuid:00000004-0000-1010-8000-3052cbcc16ee +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:schemas-upnp-org:device:Basic:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:schemas-upnp-org:device:Basic:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:dial-multiscreen-org:service:dial:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: upnp:rootdevice +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: uuid:00000004-0000-1010-8000-3052cbcc16ee +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:schemas-upnp-org:device:Basic:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:schemas-upnp-org:device:Basic:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:dial-multiscreen-org:service:dial:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: upnp:rootdevice +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::upnp:rootdevice + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: uuid:00000004-0000-1010-8000-3052cbcc16ee +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:schemas-upnp-org:device:Basic:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:schemas-upnp-org:device:Basic:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:35 GMT +EXT: +LOCATION: http://10.0.0.102:50201/dial.xml +SERVER: Linux/3.10 UPnP/1.0 Sony-BDP/2.0 +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:00000004-0000-1010-8000-3052cbcc16ee::urn:dial-multiscreen-org:service:dial:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: upnp:rootdevice +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::upnp:rootdevice +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:device:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:device:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:service:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: upnp:rootdevice +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::upnp:rootdevice +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:device:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:device:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:service:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: upnp:rootdevice +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::upnp:rootdevice +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:device:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:device:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +DATE: Sat, 06 Apr 2019 16:37:36 GMT +EXT: +LOCATION: http://10.0.0.114:8008/ssdp/device-desc.xml +OPT: "http://schemas.upnp.org/upnp/1/0/"; ns=01 +01-NLS: a484efe2-1dd1-11b2-880a-85988afcb055 +SERVER: Linux/3.8.13+, UPnP/1.0, Portable SDK for UPnP devices/1.6.18 +X-User-Agent: redsonic +ST: urn:dial-multiscreen-org:service:dial:1 +USN: uuid:946ac6b4-f481-9991-f18f-31de5b0e1e49::urn:dial-multiscreen-org:service:dial:1 +BOOTID.UPNP.ORG: 129 +CONFIGID.UPNP.ORG: 2 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:device:MediaRenderer:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:device:MediaRenderer:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:device:MediaRenderer:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:device:MediaRenderer:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:device:MediaRenderer:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:device:MediaRenderer:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:RenderingControl:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:RenderingControl:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:RenderingControl:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:RenderingControl:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:ConnectionManager:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:ConnectionManager:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:device:MediaRenderer:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:device:MediaRenderer:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:device:MediaRenderer:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:device:MediaRenderer:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:RenderingControl:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:RenderingControl:1 + + +HTTP/1.1 200 OK +EXT: +CACHE-CONTROL: max-age=1200 +SERVER: Arduino/1.0 UPNP/1.1 / +USN: uuid:38323636-4558-4dda-9188-cda0e6381cfc +ST: upnp:rootdevice +LOCATION: http://10.0.0.144:80/description.xml + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:ConnectionManager:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:ConnectionManager:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:ConnectionManager:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:ConnectionManager:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:RenderingControl:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:RenderingControl:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:RenderingControl:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:RenderingControl:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:AVTransport:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:AVTransport:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:ConnectionManager:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:ConnectionManager:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:AVTransport:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:AVTransport:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:AVTransport:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:AVTransport:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:ConnectionManager:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:ConnectionManager:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:AVTransport:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:AVTransport:1 + + +HTTP/1.1 200 OK +CACHE-CONTROL: max-age=1800 +EXT: +LOCATION: http://10.0.0.151:8080/description.xml +SERVER: KnOS/3.2 UPnP/1.0 DMP/3.5 +ST: urn:schemas-upnp-org:service:AVTransport:1 +USN: uuid:5f9ec1b3-ff59-19bb-8530-0006782430bb::urn:schemas-upnp-org:service:AVTransport:1 diff --git a/tests/deviceTest.py b/tests/device_test.py similarity index 93% rename from tests/deviceTest.py rename to tests/device_test.py index ae7a886..8eff75d 100644 --- a/tests/deviceTest.py +++ b/tests/device_test.py @@ -1,15 +1,13 @@ -import unittest -from unittest import mock import os.path - -from inspect import getsourcefile -import os.path as path import sys -import requests +import unittest +from inspect import getsourcefile +from unittest import mock +from tests.testutil import read_file -current_dir = path.dirname(path.abspath(getsourcefile(lambda: 0))) -sys.path.insert(0, current_dir[:current_dir.rfind(path.sep)]) +current_dir = os.path.dirname(os.path.abspath(getsourcefile(lambda: 0))) +sys.path.insert(0, current_dir[:current_dir.rfind(os.path.sep)]) # cannot be imported at a different position because path modification # is necessary to load the local library. # otherwise it must be installed after every change @@ -30,19 +28,14 @@ APP_LIST_URL = 'http://test:50202/appslist' -def read_file(file_name): - """ Reads a file from disk """ - __location__ = os.path.realpath(os.path.join( - os.getcwd(), os.path.dirname(__file__))) - with open(os.path.join(__location__, file_name)) as f: - return f.read() - def mock_error(*args, **kwargs): raise Exception() + def mock_nothing(*args, **kwargs): pass + def mock_discovery(*args, **kwargs): if args[0] == "urn:schemas-sony-com:service:IRCC:1": resp = SSDPResponse(None) @@ -50,7 +43,8 @@ def mock_discovery(*args, **kwargs): return [resp] return None -class MockResponse(): + +class MockResponse: class MockResponseJson: def __init__(self, data): self.data = data @@ -87,17 +81,17 @@ def mocked_requests_get(*args, **kwargs): url = args[0] print("GET for URL: {}".format(url)) if url == DMR_URL: - return MockResponse(None, 200, read_file("xml/dmr_v3.xml")) + return MockResponse(None, 200, read_file("data/dmr_v3.xml")) elif url == IRCC_URL: - return MockResponse(None, 200, read_file("xml/ircc.xml")) + return MockResponse(None, 200, read_file("data/ircc.xml")) elif url == ACTION_LIST_URL: - return MockResponse(None, 200, read_file("xml/actionlist.xml")) + return MockResponse(None, 200, read_file("data/actionlist.xml")) elif url == SYSTEM_INFORMATION_URL: - return MockResponse(None, 200, read_file("xml/getSysteminformation.xml")) + return MockResponse(None, 200, read_file("data/getSysteminformation.xml")) elif url == GET_REMOTE_COMMAND_LIST_URL: - return MockResponse(None, 200, read_file("xml/getRemoteCommandList.xml")) + return MockResponse(None, 200, read_file("data/getRemoteCommandList.xml")) elif url == APP_LIST_URL: - return MockResponse(None, 200, read_file("xml/appsList.xml")) + return MockResponse(None, 200, read_file("data/appsList.xml")) elif url == REGISTRATION_URL_LEGACY: return MockResponse({}, 200) @@ -176,7 +170,7 @@ def test_update_service_urls_v3(self, mock_ircc, mock_action_list, @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_dmr_v3(self, mock_get): - content = read_file("xml/dmr_v3.xml") + content = read_file("data/dmr_v3.xml") device = self.create_device() device._parse_dmr(content) self.verify_device_dmr(device) @@ -184,7 +178,7 @@ def test_parse_dmr_v3(self, mock_get): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_dmr_v4(self, mock_get): - content = read_file("xml/dmr_v4.xml") + content = read_file("data/dmr_v4.xml") device = self.create_device() device._parse_dmr(content) self.verify_device_dmr(device) diff --git a/tests/ssdp_test.py b/tests/ssdp_test.py new file mode 100644 index 0000000..e2d6492 --- /dev/null +++ b/tests/ssdp_test.py @@ -0,0 +1,66 @@ +import os.path as path +import sys +import unittest +from inspect import getsourcefile +from socket import timeout +from unittest import mock + +from tests.testutil import read_file_bin + +current_dir = path.dirname(path.abspath(getsourcefile(lambda: 0))) +sys.path.insert(0, current_dir[:current_dir.rfind(path.sep)]) +# cannot be imported at a different position because path modification +# is necessary to load the local library. +# otherwise it must be installed after every change +from sonyapilib.ssdp import SSDPDiscovery + +sys.path.pop(0) + + +def mock_socket(*args, **kwargs): + + class MockSocket: + def __init__(self): + self.offset = 0 + + def setsockopt(self, *args): + pass + + def sendto(self, *args): + pass + + def recv(self, size): + data = read_file_bin("data/ssdp.txt", size=size, offset=self.offset) + if not data: + raise timeout() + data = data.replace('\n', '\r\n') + encoded = data.encode() + self.offset = self.offset + len(encoded) + return encoded + + return MockSocket() + + +class SSDPDiscoveryTest(unittest.TestCase): + @mock.patch('socket.socket', side_effect=mock_socket) + def test_discover(self, mock_socket): + discovery = SSDPDiscovery() + services = discovery.discover() + self.assertEqual(len(services), 9) + + urls = ["http://10.0.0.1:49000/igd2desc.xml", + "http://10.0.0.1:49000/fboxdesc.xml", + "http://10.0.0.1:49000/igddesc.xml", + "http://10.0.0.1:49000/avmnexusdesc.xml", + "http://10.0.0.1:49000/l2tpv3.xml", + "http://10.0.0.114:8008/ssdp/device-desc.xml", + "http://10.0.0.102:50201/dial.xml", + "http://10.0.0.151:8080/description.xml", + "http://10.0.0.144:80/description.xml"] + for service in services: + self.assertTrue(service.location in urls) + self.assertTrue(service.location in str(service)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testutil.py b/tests/testutil.py new file mode 100644 index 0000000..d2c25b4 --- /dev/null +++ b/tests/testutil.py @@ -0,0 +1,18 @@ +import os.path + +__location__ = os.path.realpath(os.path.join( + os.getcwd(), os.path.dirname(__file__))) + + +def read_file(file_name): + """ Reads a file from disk """ + with open(os.path.join(__location__, file_name)) as f: + return f.read() + + +def read_file_bin(file_name, size, offset): + """ Reads a file from disk """ + with open(os.path.join(__location__, file_name)) as f: + f.seek(offset) + return f.read(size) + From 8bd811099739060595e124d5fbde890034552089 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 6 Apr 2019 23:26:00 +0200 Subject: [PATCH 100/170] added python-coveralls to travis config. --- .travis.yml | 6 ++++-- setup.py | 9 +-------- sonyapilib/ssdp.py | 33 +++++++++++++++++++-------------- test_requirements.txt | 6 ++++++ tests/ssdp_test.py | 7 +++---- tests/testutil.py | 2 +- 6 files changed, 34 insertions(+), 29 deletions(-) create mode 100644 test_requirements.txt diff --git a/.travis.yml b/.travis.yml index 9f10993..6322819 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,10 @@ language: python python: - "3.5" -cache: pip +install: + - pip install -r test_requirements.txt + - pip install . script: - - coverage run setup.py test + - py.test tests/*_test.py --cov=sonyapilib - pylint sonyapilib - coveralls \ No newline at end of file diff --git a/setup.py b/setup.py index 43ef3a1..eb90d01 100644 --- a/setup.py +++ b/setup.py @@ -33,12 +33,5 @@ 'setuptools', 'requests', 'wakeonlan' - ], - tests_require=['pytest>=3.6', - 'pytest-pep8', - 'pytest-cov', - 'python-coveralls', - 'pylint', - 'coverage>=4.4' - ] + ] ) diff --git a/sonyapilib/ssdp.py b/sonyapilib/ssdp.py index b68ec03..3d5fc29 100644 --- a/sonyapilib/ssdp.py +++ b/sonyapilib/ssdp.py @@ -16,7 +16,7 @@ class SSDPResponse: def __init__(self, response): if not response: return - + # construct a message from the request string message = email.message_from_file(StringIO(response)) @@ -37,6 +37,23 @@ def __repr__(self): class SSDPDiscovery(): # pylint: disable=too-few-public-methods """Discover devices via the ssdp protocol.""" + + @staticmethod + def _parse_response(data): + responses = {} + lines = "" + http_ok = "HTTP/1.1 200 OK" + for line in data.split('\r\n'): + if http_ok in line and lines: + response = SSDPResponse(lines) + responses[response.location] = response + lines = "" + elif http_ok not in line: + line_content = line.split(":") + if len(line_content) >= 2 and line_content[1]: + lines += line + '\r\n' + return list(responses.values()) + @staticmethod def discover(service="ssdp:all", timeout=1, retries=5, mx=3): # pylint: disable=invalid-name @@ -51,7 +68,6 @@ def discover(service="ssdp:all", timeout=1, retries=5, mx=3): 'MAN: "ssdp:discover"', 'ST: {st}', 'MX: {mx}', '', '']) # using a dict to prevent duplicated entries. - responses = {} for _ in range(0, retries): sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) @@ -72,15 +88,4 @@ def discover(service="ssdp:all", timeout=1, retries=5, mx=3): except socket.timeout: break - lines = "" - http_ok = "HTTP/1.1 200 OK" - for line in data.split('\r\n'): - if http_ok in line and len(lines) > 0: - response = SSDPResponse(lines) - responses[response.location] = response - lines = "" - elif http_ok not in line: - line_content = line.split(":") - if len(line_content) >= 2 and line_content[1]: - lines += line + '\r\n' - return list(responses.values()) + return SSDPDiscovery._parse_response(data) diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..67b727f --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,6 @@ +pytest>=3.6 +pytest-pep8 +pytest-cov +python-coveralls +pylint +coverage>=4.4 \ No newline at end of file diff --git a/tests/ssdp_test.py b/tests/ssdp_test.py index e2d6492..a5538d1 100644 --- a/tests/ssdp_test.py +++ b/tests/ssdp_test.py @@ -33,10 +33,9 @@ def recv(self, size): data = read_file_bin("data/ssdp.txt", size=size, offset=self.offset) if not data: raise timeout() - data = data.replace('\n', '\r\n') - encoded = data.encode() - self.offset = self.offset + len(encoded) - return encoded + + self.offset = self.offset + len(data) + return data return MockSocket() diff --git a/tests/testutil.py b/tests/testutil.py index d2c25b4..bdc1c7e 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -12,7 +12,7 @@ def read_file(file_name): def read_file_bin(file_name, size, offset): """ Reads a file from disk """ - with open(os.path.join(__location__, file_name)) as f: + with open(os.path.join(__location__, file_name), 'rb') as f: f.seek(offset) return f.read(size) From f1668d80df3fb97b0a555e5dbee8e9208162016a Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 7 Apr 2019 00:37:56 +0200 Subject: [PATCH 101/170] added git attributes --- .gitattributes | 6 ++++++ .travis.yml | 1 + 2 files changed, 7 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5de9a7a --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# Set the default behavior, in case people don't have core.autocrlf set. +* crlf_text eol=crlf + +# Explicitly declare text files you want to always be normalized and converted +# to native line endings on checkout. +ssdp.text crlf_text \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 6322819..4a4d3b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - "3.5" +cache: pip install: - pip install -r test_requirements.txt - pip install . From 7ddf036eba61fd112074328012f103aa4593c494 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 7 Apr 2019 03:25:43 +0200 Subject: [PATCH 102/170] added more tests --- sonyapilib/device.py | 61 ++++++++------ tests/device_test.py | 193 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 211 insertions(+), 43 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 8b89b40..955b176 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -1,17 +1,17 @@ """ Sony Media player lib """ +import base64 +import json +import logging +import uuid +import xml.etree.ElementTree from enum import Enum from urllib.parse import ( urljoin, urlparse, quote, ) -import base64 -import json -import logging -import uuid -import xml.etree.ElementTree import jsonpickle import requests @@ -20,7 +20,6 @@ from sonyapilib import ssdp from sonyapilib.xml_helper import find_in_xml - _LOGGER = logging.getLogger(__name__) TIMEOUT = 5 @@ -97,7 +96,7 @@ def __init__(self, host, nickname): self.name = None self.cookies = None self.mac = None - self.is_v4 = False + self.api_version = 0 self.uuid = uuid.uuid4() ircc_base = "http://{0.host}:{0.ircc_port}".format(self) @@ -148,7 +147,7 @@ def _update_service_urls(self): try: self._parse_dmr(response.text) - if self.is_v4: + if self.api_version > 3: # todo implement this pass else: @@ -254,7 +253,7 @@ def _parse_dmr(self, data): if WEBAPI_SERVICETYPE not in data: return - self.is_v4 = True + self.api_version = 4 device_info_name = "{0}X_ScalarWebAPI_DeviceInfo".format( URN_SCALAR_WEB_API_DEVICE_INFO ) @@ -288,24 +287,32 @@ def _update_commands(self): _LOGGER.error("Registration necessary to read command list.") return - if self.is_v4: - # todo refactor to method - action_name = "getRemoteControllerInfo" - action = self.actions[action_name] - json_data = self._create_api_json(action.value) - - resp = self._send_http( - action.url, HttpMethod.POST, json=json_data, headers={} - ).json() - if resp and not resp.get('error'): - # todo parse this into the old structure. - self.commands = resp.get('result')[1] - else: - _LOGGER.error("JSON request error: %s", - json.dumps(resp, indent=4)) + if self.api_version > 3: + self._parse_command_list_v4() else: self._parse_command_list() + def _parse_command_list_v4(self): + action_name = "getRemoteControllerInfo" + action = self.actions[action_name] + json_data = self._create_api_json(action.value) + + response = self._send_http( + action.url, HttpMethod.POST, json=json_data, headers={} + ) + + if not response: + _LOGGER.error("no response received") + return + + json_resp = response.json() + if json_resp and not json_resp.get('error'): + # todo parse this into the old structure. + self.commands = json_resp.get('result')[1] + else: + _LOGGER.error("JSON request error: %s", + json.dumps(json_resp, indent=4)) + def _parse_command_list(self): """Parse the list of available command in devices with the legacy api.""" url = self._get_action("getRemoteCommandList").url @@ -448,6 +455,7 @@ def _send_command(self, name): def _get_action(self, name): """Get the action object for the action with the given name""" if name not in self.actions and not self.actions: + self._init_device() if name not in self.actions and not self.actions: raise ValueError('Failed to read action list from device.') @@ -483,7 +491,7 @@ def _register_v3(self, registration_action): return AuthenticationResult.SUCCESS def _register_v4(self, registration_action): - authorization = self._create_api_json("actRegister", 13) + authorization = self._create_api_json("actRegister") try: headers = { @@ -542,6 +550,9 @@ def send_authentication(self, pin): if registration_action.mode < 3: return True + if not pin: + return False + self.pin = pin self._recreate_authentication() result = self.register() diff --git a/tests/device_test.py b/tests/device_test.py index 8eff75d..eada9e7 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -4,6 +4,8 @@ from inspect import getsourcefile from unittest import mock +from requests import HTTPError + from tests.testutil import read_file current_dir = os.path.dirname(os.path.abspath(getsourcefile(lambda: 0))) @@ -25,8 +27,11 @@ REGISTRATION_URL_LEGACY = 'http://192.168.240.4:50002/register' REGISTRATION_URL_V4 = 'http://192.168.178.23/sony/accessControl' REGISTRATION_URL_V4_FAIL = 'http://192.168.178.22/sony/accessControl' +REGISTRATION_URL_V4_FAIL_401 = 'http://192.168.178.25/sony/accessControl' +REGISTRATION_URL_V3_FAIL_401 = 'http://192.168.240.7:50002/register' APP_LIST_URL = 'http://test:50202/appslist' - +SOAP_URL = 'http://test/soap' +GET_REMOTE_CONTROLLER_INFO_URL = "http://test/getRemoteControllerInfo" def mock_error(*args, **kwargs): raise Exception() @@ -36,6 +41,10 @@ def mock_nothing(*args, **kwargs): pass +def mock_register_success(*args, **kwargs): + return AuthenticationResult.SUCCESS + + def mock_discovery(*args, **kwargs): if args[0] == "urn:schemas-sony-com:service:IRCC:1": resp = SSDPResponse(None) @@ -59,6 +68,8 @@ def __init__(self, json_data, status_code, text=None, cookies=None): self.status_code = status_code self.text = text self.cookies = cookies + if text: + self.content = text.encode() def json(self): return self.json_obj @@ -67,7 +78,12 @@ def get(self): pass def raise_for_status(self): - pass + if self.status_code == 200: + return + error = HTTPError() + error.response = self + raise error + def mocked_requests_post(*args, **kwargs): url = args[0] @@ -76,6 +92,11 @@ def mocked_requests_post(*args, **kwargs): return MockResponse({}, 200) elif url == REGISTRATION_URL_V4_FAIL: return MockResponse({"error": 402}, 200) + elif url == REGISTRATION_URL_V4_FAIL_401: + MockResponse(None, 401).raise_for_status() + elif url == SOAP_URL: + return MockResponse({}, 200, "data") + def mocked_requests_get(*args, **kwargs): url = args[0] @@ -94,9 +115,16 @@ def mocked_requests_get(*args, **kwargs): return MockResponse(None, 200, read_file("data/appsList.xml")) elif url == REGISTRATION_URL_LEGACY: return MockResponse({}, 200) + elif url == REGISTRATION_URL_V3_FAIL_401: + MockResponse(None, 401).raise_for_status() + elif url == GET_REMOTE_CONTROLLER_INFO_URL: + return MockResponse(None, 200) + else: + raise ValueError("Unknown url requested: {}".format(url)) return MockResponse(None, 404) + class SonyDeviceTest(unittest.TestCase): @mock.patch('sonyapilib.device.SonyDevice._update_service_urls', side_effect=mock_nothing) @@ -174,7 +202,7 @@ def test_parse_dmr_v3(self, mock_get): device = self.create_device() device._parse_dmr(content) self.verify_device_dmr(device) - self.assertFalse(device.is_v4) + self.assertLess(device.api_version, 4) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_dmr_v4(self, mock_get): @@ -182,7 +210,7 @@ def test_parse_dmr_v4(self, mock_get): device = self.create_device() device._parse_dmr(content) self.verify_device_dmr(device) - self.assertTrue(device.is_v4) + self.assertGreater(device.api_version, 3) self.assertEqual( device.actions["register"].url, REGISTRATION_URL_V4) self.assertEqual(device.actions["register"].mode, 4) @@ -202,6 +230,12 @@ def test_parse_ircc(self, mock_get): self.assertEqual( device.control_url, 'http://test:50001/upnp/control/IRCC') + def test_parse_action_list_error(self): + # just make sure nothing crashes + device = self.create_device() + device.actionlist_url = ACTION_LIST_URL + device._parse_action_list() + @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_action_list(self, mock_get): device = self.create_device() @@ -263,8 +297,13 @@ def test_update_commands_v3(self, mock_parse_cmd_list): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_update_commands_v4(self, mock_get): - # todo implement parsing of command list - pass + device = self.create_device() + device.pin = 1234 + device.api_version = 4 + action = XmlApiObject({}) + action.url = GET_REMOTE_CONTROLLER_INFO_URL + device.actions["getRemoteControllerInfo"] = action + device._update_commands() @mock.patch('requests.get', side_effect=mocked_requests_get) def test_update_applist(self, mock_get): @@ -282,6 +321,14 @@ def test_update_applist(self, mock_get): self.assertTrue(app in app_list) self.assertEqual(len(device.apps), len(app_list)) + def test_recreate_authentication_no_auth(self): + versions = [1, 2] + for version in versions: + device = self.create_device() + self.add_register_to_device(device, version) + device._recreate_authentication() + self.assertEqual(len(device.headers), 0) + def test_recreate_authentication_v3(self): device = self.create_device() device.pin = 1234 @@ -289,8 +336,7 @@ def test_recreate_authentication_v3(self): device._recreate_authentication() self.assertEqual(device.headers["Authorization"], "Basic OjEyMzQ=") - self.assertEqual( - device.headers["X-CERS-DEVICE-ID"], device.get_device_id()) + self.assertEqual(device.headers["X-CERS-DEVICE-ID"], device.get_device_id()) def test_recreate_authentication_v4(self): device = self.create_device() @@ -300,6 +346,7 @@ def test_recreate_authentication_v4(self): self.assertEqual(device.headers["Authorization"], "Basic OjEyMzQ=") self.assertEqual(device.headers["Connection"], "keep-alive") + self.verify_cookies(device.cookies) def test_recreate_authentication_v4_psk(self): # todo implement psk @@ -310,7 +357,7 @@ def test_register_no_auth(self, mocked_get): versions = [1, 2] for version in versions: result = self.register_with_version(version) - self.assertEqual(result, AuthenticationResult.SUCCESS) + self.assertEqual(result[0], AuthenticationResult.SUCCESS) @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) @mock.patch('requests.get', side_effect=mocked_requests_get) @@ -319,29 +366,97 @@ def test_register_not_supported(self, mocked_get, mocked_init_device): self.register_with_version(5) self.assertEqual(mocked_init_device.call_count, 0) + def verify_register_fail(self, version, auth_result, mocked_init_device, url=None): + result = self.register_with_version(version, url) + self.assertEqual(result[0], auth_result) + self.assertEqual(mocked_init_device.call_count, 0) + @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) def test_register_fail_http_timeout(self, mocked_init_device): versions = [1, 2, 3, 4] for version in versions: - result = self.register_with_version(version) - self.assertEqual(result, AuthenticationResult.ERROR) - self.assertEqual(mocked_init_device.call_count, 0) + self.verify_register_fail(version, AuthenticationResult.ERROR, mocked_init_device) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch('requests.post', side_effect=mocked_requests_post) + @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + def test_register_fail_pin_needed(self, mocked_init_device, mock_request_get_401, mock_request_post_401): + self.verify_register_fail(3, AuthenticationResult.PIN_NEEDED, mocked_init_device, REGISTRATION_URL_V3_FAIL_401) + self.verify_register_fail(4, AuthenticationResult.PIN_NEEDED, mocked_init_device, REGISTRATION_URL_V4_FAIL_401) @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_register_success_v3(self, mocked_requests_get, mocked_init_device): result = self.register_with_version(3) - self.assertEqual(result, AuthenticationResult.SUCCESS) + self.assertEqual(result[0], AuthenticationResult.SUCCESS) self.assertEqual(mocked_init_device.call_count, 1) @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) @mock.patch('requests.post', side_effect=mocked_requests_post) def test_register_no_json_v4(self, mocked_requests_post, mocked_init_device): result = self.register_with_version(4, REGISTRATION_URL_V4_FAIL) - self.assertEqual(result, AuthenticationResult.ERROR) + self.assertEqual(result[0], AuthenticationResult.ERROR) self.assertEqual(mocked_init_device.call_count, 0) - def add_register_to_device(self, device, mode): + @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_register_success_v4(self, mocked_requests_post, mocked_init_device): + result = self.register_with_version(4, REGISTRATION_URL_V4) + self.assertEqual(result[0], AuthenticationResult.SUCCESS) + self.assertEqual(mocked_init_device.call_count, 1) + self.verify_cookies(result[1].cookies) + + @mock.patch('sonyapilib.device.SonyDevice.register', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._recreate_authentication', side_effect=mock_nothing) + def test_send_authentication_no_auth(self, mock_register, mock_recreate_auth): + versions = [[1, True], [2, True], [3, False], [4, False]] + for version in versions: + device = self.create_device() + self.add_register_to_device(device, version[0]) + self.assertEqual(device.send_authentication(0), version[1]) + self.assertEqual(mock_register.call_count, 0) + self.assertEqual(mock_recreate_auth.call_count, 0) + + @mock.patch('sonyapilib.device.SonyDevice.register', side_effect=mock_register_success) + @mock.patch('sonyapilib.device.SonyDevice._recreate_authentication', side_effect=mock_nothing) + def test_send_authentication_with_auth(self, mock_register, mock_recreate_auth): + versions = [3, 4] + for version in versions: + device = self.create_device() + self.add_register_to_device(device, version) + self.assertTrue(device.send_authentication(1234)) + self.assertEqual(mock_register.call_count, 1) + self.assertEqual(mock_recreate_auth.call_count, 1) + mock_register.call_count = 0 + mock_recreate_auth.call_count = 0 + + @mock.patch('sonyapilib.device.SonyDevice._send_command', side_effect=mock_nothing) + def test_commands(self, mock_send_command): + device = self.create_device() + methods = ["up", "confirm", "down", "right", "left", "home", "options", "returns", "num1", "num2", "num3", + "num4", + "num5", "num6", "num7", "num8", "num9", "num0", "display", "audio", "sub_title", "favorites", + "yellow", + "blue", "red", "green", "play", "stop", "pause", "rewind", "forward", "prev", "next", "replay", + "advance", + "angle", "top_menu", "pop_up_menu", "eject", "karaoke", "netflix", "mode_3d", "zoom_in", "zoom_out", + "browser_back", "browser_forward", "browser_bookmark_list", "list"] + for method in methods: + cmd_name = ''.join(x.capitalize() or '_' for x in method.split('_')) + # method cannot be named return + if method == "returns": + cmd_name = "Return" + elif method == "mode_3d": + cmd_name = "Mode3D" + + getattr(device, method)() + self.assertEqual(mock_send_command.call_count, 1) + self.assertEqual(mock_send_command.mock_calls[0][1][0], cmd_name) + mock_send_command.call_count = 0 + mock_send_command.mock_calls.clear() + + @staticmethod + def add_register_to_device(device, mode): register_action = XmlApiObject({}) register_action.mode = mode if mode < 4: @@ -357,15 +472,57 @@ def register_with_version(self, version, reg_url=""): device.actions["register"].url = reg_url result = device.register() - return result + return [result, device] + + def test_post_soap_request_invalid(self): + device = self.create_device() + params = "foobar" + self.assertFalse(device._post_soap_request(SOAP_URL, params, params)) + + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_post_soap_request(self, mocked_requests_post): + params = "foobar" + data = """ + + + {0} + + """.format("foobar") + + device = self.create_device() + self.assertTrue(device._post_soap_request(SOAP_URL, params, params)) + mock_call = mocked_requests_post.mock_calls[0][2] + headers = mock_call["headers"] + self.assertEqual(headers['SOAPACTION'], '"{}"'.format(params)) + self.assertEqual(headers['Content-Type'], "text/xml") + self.assertEqual(mock_call["data"], data) + self.assertEqual(mocked_requests_post.call_count, 1) - def create_device(self): - sonyapilib.device.TIMEOUT = 1 + @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + def test_get_action(self, mock_init_device): + device = self.create_device() + action = XmlApiObject({}) + action.name = "test" + with self.assertRaises(ValueError): + device._get_action(action.name) + self.assertEqual(mock_init_device.call_count, 1) + device.actions[action.name] = action + self.assertEqual(device._get_action(action.name), action) + + @staticmethod + def create_device(): + sonyapilib.device.TIMEOUT = 0.1 return SonyDevice("test", "test") def verify_device_dmr(self, device): self.assertEqual(device.av_transport_url, 'http://test:52323/upnp/control/AVTransport') + @staticmethod + def verify_cookies(device): + pass # todo implement cookie verification + + if __name__ == '__main__': unittest.main() From 12045fe8b8e0f17414b86a8a17d509f39cfc8a14 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 7 Apr 2019 20:28:44 +0200 Subject: [PATCH 103/170] added commands for v4. --- setup.py | 8 ++++ sonyapilib/device.py | 80 +++++++++++++++++++++++------------- tests/device_test.py | 96 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 149 insertions(+), 35 deletions(-) diff --git a/setup.py b/setup.py index eb90d01..3df2cb0 100644 --- a/setup.py +++ b/setup.py @@ -33,5 +33,13 @@ 'setuptools', 'requests', 'wakeonlan' + ], + tests_require=[ + 'pytest>=3.6', + 'pytest-pep8', + 'pytest-cov', + 'python-coveralls', + 'pylint', + 'coverage>=4.4' ] ) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 955b176..f684fc7 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -87,8 +87,10 @@ def __init__(self, host, nickname): self.dmr_port = 52323 self.ircc_port = 50001 + # actions are thing like getting status self.actions = {} self.headers = {} + # commands are alike to buttons on the remote self.commands = {} self.apps = {} @@ -105,6 +107,7 @@ def __init__(self, host, nickname): self.dmr_url = "http://{0.host}:{0.dmr_port}/dmr.xml".format(self) self.app_url = "http://{0.host}:{0.app_port}".format(self) + self.base_url = "http://{0.host}/sony/".format(self) def _init_device(self): self._update_service_urls() @@ -265,19 +268,20 @@ def _parse_dmr(self, data): ] for device in find_in_xml(xml_data, search_params): for xml_url in device: - base_url = xml_url.text - if not base_url.endswith("/"): - base_url = "{}/".format(base_url) + self.base_url = xml_url.text + if not self.base_url.endswith("/"): + self.base_url = "{}/".format(self.base_url) action = XmlApiObject({}) - action.url = urljoin(base_url, "accessControl") + action.url = urljoin(self.base_url, "accessControl") action.mode = 4 self.actions["register"] = action action = XmlApiObject({}) - action.url = urljoin(base_url, "system") + action.url = urljoin(self.base_url, "system") action.value = "getRemoteControllerInfo" self.actions["getRemoteCommandList"] = action + self.control_url = urljoin(self.base_url, "IRCC") def _update_commands(self): """Update the list of commands.""" @@ -566,37 +570,59 @@ def wakeonlan(self, broadcast='255.255.255.255'): def get_playing_status(self): """Get the status of playback from the device""" - data = """ - 0 - """ - - action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" - - content = self._post_soap_request( - url=self.av_transport_url, params=data, action=action) - if not content: - return "OFF" - return find_in_xml(content, [".//CurrentTransportState"]).text + if self.api_version < 4: + data = """ + 0 + """ + + action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" + + content = self._post_soap_request( + url=self.av_transport_url, params=data, action=action) + if not content: + return "OFF" + return find_in_xml(content, [".//CurrentTransportState"]).text + return_value = {} + resp = self._send_http(urljoin(self.base_url, "avContent"), + self._create_api_json("getPlayingContentInfo")) + if resp is not None and not resp.get('error'): + playing = resp.get('result')[0] + # todo get the playing status and return it. + return_value['programTitle'] = playing.get('programTitle') + return return_value def get_power_status(self): """Checks if the device is online.""" - url = self.actionlist_url + if self.api_version < 4: + url = self.actionlist_url + try: + self._send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) + except requests.exceptions.RequestException as ex: + _LOGGER.debug(ex) + return False + return True try: - # todo parse response - self._send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) - except requests.exceptions.RequestException as ex: - _LOGGER.debug(ex) - return False - return True + resp = self._send_http(urljoin(self.base_url, "system"), + self._create_api_json("getPowerStatus")) + if resp is not None and not resp.get('error'): + power_data = resp.get('result')[0] + return power_data.get('status') != "off" + except requests.RequestException: + pass + return False def start_app(self, app_name): """Start an app by name""" # sometimes device does not start app if already running one - # todo add support for v4 self.home() - url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) - data = "LOCATION: {0}/run".format(url) - self._send_http(url, HttpMethod.POST, data=data) + + if self.api_version < 4: + url = "{0}/apps/{1}".format(self.app_url, self.apps[app_name].id) + data = "LOCATION: {0}/run".format(url) + self._send_http(url, HttpMethod.POST, data=data) + else: + # todo add support for v4 + pass def power(self, power_on, broadcast=None): """Powers the device on or shuts it off.""" diff --git a/tests/device_test.py b/tests/device_test.py index eada9e7..a26fbfa 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -30,9 +30,11 @@ REGISTRATION_URL_V4_FAIL_401 = 'http://192.168.178.25/sony/accessControl' REGISTRATION_URL_V3_FAIL_401 = 'http://192.168.240.7:50002/register' APP_LIST_URL = 'http://test:50202/appslist' +APP_START_URL_LEGACY = 'http://test:50202/apps/' SOAP_URL = 'http://test/soap' GET_REMOTE_CONTROLLER_INFO_URL = "http://test/getRemoteControllerInfo" + def mock_error(*args, **kwargs): raise Exception() @@ -96,7 +98,10 @@ def mocked_requests_post(*args, **kwargs): MockResponse(None, 401).raise_for_status() elif url == SOAP_URL: return MockResponse({}, 200, "data") - + elif APP_START_URL_LEGACY in url: + return MockResponse(None, 200) + else: + raise ValueError("Unknown url requested: {}".format(url)) def mocked_requests_get(*args, **kwargs): url = args[0] @@ -265,7 +270,7 @@ def test_parse_system_information(self, mock_get): device._parse_system_information() self.assertEqual(device.mac, "30-52-cb-cc-16-ee") - def prepare_test_command_list(self): + def prepare_test_action_list(self): device = self.create_device() data = XmlApiObject({}) data.url = GET_REMOTE_COMMAND_LIST_URL @@ -273,12 +278,12 @@ def prepare_test_command_list(self): return device def test_parse_command_list_error(self): - device = self.prepare_test_command_list() + device = self.prepare_test_action_list() device._parse_command_list() @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_command_list(self, mock_get): - device = self.prepare_test_command_list() + device = self.prepare_test_action_list() device._parse_command_list() self.assertEqual(len(device.commands), 48) @@ -305,20 +310,66 @@ def test_update_commands_v4(self, mock_get): device.actions["getRemoteControllerInfo"] = action device._update_commands() + def start_app_legacy(self, device, app_name, mock_post, mock_send_command): + versions = [1, 2, 3] + apps = { + "Video Explorer": "com.sony.videoexplorer", + "Music Explorer": "com.sony.musicexplorer", + "Video Player": "com.sony.videoplayer", + "Music Player": "com.sony.musicplayer", + "PlayStation Video": "com.sony.videounlimited", + "Amazon Prime Video": "com.sony.iptv.4976", + "Netflix": "com.sony.iptv.type.NRDP", + "Rakuten TV": "com.sony.iptv.3479", + "Tagesschau": "com.sony.iptv.type.EU-TAGESSCHAU_6x3", + "Functions with Gracenote ended": "com.sony.iptv.6317", + "watchmi Themenkanäle": "com.sony.iptv.4766", + "Netzkino": "com.sony.iptv.4742", + "MUBI": "com.sony.iptv.5498", + "WWE Network": "com.sony.iptv.4340", + "DW for Smart TV": "com.sony.iptv.4968", + "YouTube": "com.sony.iptv.type.ytleanback", + "uStudio": "com.sony.iptv.4386", + "Meteonews TV": "com.sony.iptv.3487", + "Digital Concert Hall": "com.sony.iptv.type.WW-BERLINPHIL_NBIV", + "Activate Enhanced Features": "com.sony.iptv.4834" + } + + for version in versions: + device.api_version = version + device.start_app(app_name) + + self.assertEqual(mock_post.call_count, 1) + self.assertEqual(mock_send_command.call_count, 1) + + url = APP_START_URL_LEGACY + apps[app_name] + self.assertEqual(url, mock_post.call_args[0][0]) + mock_send_command.call_count = 0 + mock_post.call_count = 0 + mock_post.mock_calls.clear() + + def start_app_v4(self, device, app_name): + device.api_version = 4 + # todo + + @mock.patch('sonyapilib.device.SonyDevice._send_command', side_effect=mock_nothing) + @mock.patch('requests.post', side_effect=mocked_requests_post) @mock.patch('requests.get', side_effect=mocked_requests_get) - def test_update_applist(self, mock_get): + def test_update_applist(self, mock_get, mock_post, mock_send_command): device = self.create_device() app_list = [ "Video Explorer", "Music Explorer", "Video Player", "Music Player", - "PlayStation Video", "Amazon Prime Video", "Netflix", "Rakuten TV", - "Tagesschau", "Functions with Gracenote ended", "watchmi Themenkanäle", + "PlayStation Video", "Amazon Prime Video", "Netflix", "Rakuten TV", + "Tagesschau", "Functions with Gracenote ended", "watchmi Themenkanäle", "Netzkino", "MUBI", "WWE Network", "DW for Smart TV", "YouTube", "uStudio", "Meteonews TV", "Digital Concert Hall", "Activate Enhanced Features" ] device._update_applist() - for app in device.apps: + for app in device.get_apps(): self.assertTrue(app in app_list) + self.start_app_legacy(device, app, mock_post, mock_send_command) + self.start_app_v4(device, app) self.assertEqual(len(device.apps), len(app_list)) def test_recreate_authentication_no_auth(self): @@ -510,6 +561,35 @@ def test_get_action(self, mock_init_device): device.actions[action.name] = action self.assertEqual(device._get_action(action.name), action) + @mock.patch('sonyapilib.device.SonyDevice._send_req_ircc', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + def test_send_command_error(self, mock_init_device, mock_send_req_ircc): + device = self.create_device() + with self.assertRaises(ValueError): + device._send_command("test") + self.create_command_list(device) + with self.assertRaises(ValueError): + device._send_command("foo") + device._send_command("test") + self.assertEqual(mock_send_req_ircc.call_count, 1) + + @mock.patch('sonyapilib.device.SonyDevice._post_soap_request', side_effect=mock_nothing) + def test_send_req_ircc(self, mock_post_soap_request): + device = self.create_device() + params = "foobar" + data = """ + {0} + """.format(params) + device._send_req_ircc(params) + self.assertEqual(mock_post_soap_request.call_count, 1) + self.assertEqual(mock_post_soap_request.call_args_list[0][1]['params'], data) + + @staticmethod + def create_command_list(device): + command = XmlApiObject({}) + command.name = "test" + device.commands[command.name] = command + @staticmethod def create_device(): sonyapilib.device.TIMEOUT = 0.1 From 76e0326f6fd85b750a54341853c489d58ae7d08d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 7 Apr 2019 20:52:52 +0200 Subject: [PATCH 104/170] Added apps for v4 --- sonyapilib/device.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index f684fc7..d7f0a93 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -172,8 +172,6 @@ def _parse_action_list(self): if action.name == "register": # the authentication later on is based on the device id and the mac - # todo maybe refactor this to requests - # http://docs.python-requests.org/en/master/_modules/requests/api/?highlight=param action.url = "{0}?name={1}®istrationType=initial&deviceId={2}".format( action.url, quote(self.nickname), @@ -331,9 +329,12 @@ def _parse_command_list(self): def _update_applist(self): """Update the list of apps which are supported by the device.""" - url = self.app_url + "/appslist" + if self.api_version < 4: + url = self.app_url + "/appslist" + else: + url = 'http://{}/DIAL/sony/applist'.format(self.host) + response = self._send_http(url, method=HttpMethod.GET) - # todo add support for v4 if response: for app in find_in_xml(response.text, [(".//app", True)]): data = XmlApiObject({ @@ -621,8 +622,8 @@ def start_app(self, app_name): data = "LOCATION: {0}/run".format(url) self._send_http(url, HttpMethod.POST, data=data) else: - # todo add support for v4 - pass + url = 'http://{}/DIAL/apps/{}'.format(self.host, self.apps[app_name].id) + self._send_http(url, HttpMethod.POST) def power(self, power_on, broadcast=None): """Powers the device on or shuts it off.""" From bc716c17fba3221003d7ffc0a6d995c54a3d826b Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 7 Apr 2019 21:25:45 +0200 Subject: [PATCH 105/170] added test for power status. --- sonyapilib/device.py | 15 ++++++++------ tests/device_test.py | 49 ++++++++++++++++++++++++++++++++------------ 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index d7f0a93..579ff95 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -584,8 +584,8 @@ def get_playing_status(self): return "OFF" return find_in_xml(content, [".//CurrentTransportState"]).text return_value = {} - resp = self._send_http(urljoin(self.base_url, "avContent"), - self._create_api_json("getPlayingContentInfo")) + resp = self._send_http(urljoin(self.base_url, "avContent"), HttpMethod.POST, + json=self._create_api_json("getPlayingContentInfo")) if resp is not None and not resp.get('error'): playing = resp.get('result')[0] # todo get the playing status and return it. @@ -603,10 +603,13 @@ def get_power_status(self): return False return True try: - resp = self._send_http(urljoin(self.base_url, "system"), - self._create_api_json("getPowerStatus")) - if resp is not None and not resp.get('error'): - power_data = resp.get('result')[0] + resp = self._send_http(urljoin(self.base_url, "system"), HttpMethod.POST, + json=self._create_api_json("getPowerStatus")) + if not resp: + return False + json_data = resp.json() + if not json_data.get('error'): + power_data = json_data.get('result')[0] return power_data.get('status') != "off" except requests.RequestException: pass diff --git a/tests/device_test.py b/tests/device_test.py index a26fbfa..bc75956 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -3,6 +3,9 @@ import unittest from inspect import getsourcefile from unittest import mock +from urllib.parse import ( + urljoin +) from requests import HTTPError @@ -33,6 +36,7 @@ APP_START_URL_LEGACY = 'http://test:50202/apps/' SOAP_URL = 'http://test/soap' GET_REMOTE_CONTROLLER_INFO_URL = "http://test/getRemoteControllerInfo" +BASE_URL = 'http://test/sony' def mock_error(*args, **kwargs): @@ -55,18 +59,18 @@ def mock_discovery(*args, **kwargs): return None -class MockResponse: - class MockResponseJson: - def __init__(self, data): - self.data = data - - def get(self, key): - if key in self.data: - return self.data[key] - return None +class MockResponseJson: + def __init__(self, data): + self.data = data + + def get(self, key): + if key in self.data: + return self.data[key] + return None +class MockResponse: def __init__(self, json_data, status_code, text=None, cookies=None): - self.json_obj = self.MockResponseJson(json_data) + self.json_obj = MockResponseJson(json_data) self.status_code = status_code self.text = text self.cookies = cookies @@ -76,9 +80,6 @@ def __init__(self, json_data, status_code, text=None, cookies=None): def json(self): return self.json_obj - def get(self): - pass - def raise_for_status(self): if self.status_code == 200: return @@ -98,11 +99,15 @@ def mocked_requests_post(*args, **kwargs): MockResponse(None, 401).raise_for_status() elif url == SOAP_URL: return MockResponse({}, 200, "data") + elif url == urljoin(BASE_URL, 'system'): + result = MockResponseJson({"status": "on"}) + return MockResponse({"result": [result]}, 200) elif APP_START_URL_LEGACY in url: return MockResponse(None, 200) else: raise ValueError("Unknown url requested: {}".format(url)) + def mocked_requests_get(*args, **kwargs): url = args[0] print("GET for URL: {}".format(url)) @@ -584,6 +589,24 @@ def test_send_req_ircc(self, mock_post_soap_request): self.assertEqual(mock_post_soap_request.call_count, 1) self.assertEqual(mock_post_soap_request.call_args_list[0][1]['params'], data) + def test_get_power_status_false(self): + versions = [1, 2, 3, 4] + device = self.create_device() + for version in versions: + device.api_version = version + self.assertFalse(device.get_power_status()) + + @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_get_power_status_true(self, mocked_post, mocked_get): + versions = [1, 2, 3, 4] + device = self.create_device() + device.actionlist_url = ACTION_LIST_URL + device.base_url = BASE_URL + for version in versions: + device.api_version = version + self.assertTrue(device.get_power_status()) + @staticmethod def create_command_list(device): command = XmlApiObject({}) From 447a4b0ceda456786a56b4760df8e888b97918ac Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Apr 2019 15:17:24 +0200 Subject: [PATCH 106/170] Added most tests possible before getting data from v4 device. --- tests/device_test.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/tests/device_test.py b/tests/device_test.py index bc75956..f65ddd7 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -33,6 +33,7 @@ REGISTRATION_URL_V4_FAIL_401 = 'http://192.168.178.25/sony/accessControl' REGISTRATION_URL_V3_FAIL_401 = 'http://192.168.240.7:50002/register' APP_LIST_URL = 'http://test:50202/appslist' +APP_LIST_URL_V4 = 'http://test/DIAL/sony/applist' APP_START_URL_LEGACY = 'http://test:50202/apps/' SOAP_URL = 'http://test/soap' GET_REMOTE_CONTROLLER_INFO_URL = "http://test/getRemoteControllerInfo" @@ -121,7 +122,7 @@ def mocked_requests_get(*args, **kwargs): return MockResponse(None, 200, read_file("data/getSysteminformation.xml")) elif url == GET_REMOTE_COMMAND_LIST_URL: return MockResponse(None, 200, read_file("data/getRemoteCommandList.xml")) - elif url == APP_LIST_URL: + elif url == APP_LIST_URL or url == APP_LIST_URL_V4: # todo make sure lists are equal return MockResponse(None, 200, read_file("data/appsList.xml")) elif url == REGISTRATION_URL_LEGACY: return MockResponse({}, 200) @@ -370,12 +371,17 @@ def test_update_applist(self, mock_get, mock_post, mock_send_command): "uStudio", "Meteonews TV", "Digital Concert Hall", "Activate Enhanced Features" ] - device._update_applist() - for app in device.get_apps(): - self.assertTrue(app in app_list) - self.start_app_legacy(device, app, mock_post, mock_send_command) - self.start_app_v4(device, app) - self.assertEqual(len(device.apps), len(app_list)) + versions = [1, 2, 3, 4] + for version in versions: + device.api_version = version + device._update_applist() + for app in device.get_apps(): + self.assertTrue(app in app_list) + if device.api_version < 4: + self.start_app_legacy(device, app, mock_post, mock_send_command) + else: + self.start_app_v4(device, app) + self.assertEqual(len(device.apps), len(app_list)) def test_recreate_authentication_no_auth(self): versions = [1, 2] @@ -607,6 +613,24 @@ def test_get_power_status_true(self, mocked_post, mocked_get): device.api_version = version self.assertTrue(device.get_power_status()) + @mock.patch('sonyapilib.device.SonyDevice._send_command', side_effect=mock_nothing) + def test_power_off(self, mock_send_command): + device = self.create_device() + device.power(False) + self.assertEqual(mock_send_command.call_count, 1) + self.assertEqual(mock_send_command.mock_calls[0][1][0], "Power") + + @mock.patch('sonyapilib.device.SonyDevice.get_power_status', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._send_command', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.wakeonlan', side_effect=mock_nothing) + def test_power_off(self, mock_wake_on_lan, mock_send_command, mock_get_power_status): + device = self.create_device() + device.power(True) + self.assertEqual(mock_send_command.call_count, 1) + self.assertEqual(mock_wake_on_lan.call_count, 1) + self.assertEqual(mock_send_command.mock_calls[0][1][0], "Power") + pass + @staticmethod def create_command_list(device): command = XmlApiObject({}) From c751f2925fb8296df262ddae2d3eeb15743eb768 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Apr 2019 16:07:18 +0200 Subject: [PATCH 107/170] tests for playing status. --- examples/pair_and_apps.py | 29 +++++++---- examples/simple_example.py | 39 -------------- sonyapilib/device.py | 3 +- tests/data/playing_status_legacy_no_media.xml | 11 ++++ tests/data/playing_status_legacy_playing.xml | 11 ++++ tests/device_test.py | 51 ++++++++++++------- 6 files changed, 76 insertions(+), 68 deletions(-) delete mode 100644 examples/simple_example.py create mode 100644 tests/data/playing_status_legacy_no_media.xml create mode 100644 tests/data/playing_status_legacy_playing.xml diff --git a/examples/pair_and_apps.py b/examples/pair_and_apps.py index 35bd635..aeb19f5 100644 --- a/examples/pair_and_apps.py +++ b/examples/pair_and_apps.py @@ -1,36 +1,45 @@ from sonyapilib.device import SonyDevice +CONFIG_FILE = "bluray.json" + def save_device(): data = device.save_to_json() - text_file = open("bluray.json", "w") + text_file = open(CONFIG_FILE, "w") text_file.write(data) text_file.close() -if __name__ == "__main__": - stored_config = "bluray.json" +def load_device(): + import os device = None - import os.path - if os.path.exists(stored_config): - with open(stored_config, 'r') as content_file: + if os.path.exists(CONFIG_FILE): + with open(CONFIG_FILE, 'r') as content_file: json_data = content_file.read() device = SonyDevice.load_from_json(json_data) - else: + return device + + +if __name__ == "__main__": + device = load_device() + if not device: # device must be on for registration host = "10.0.0.102" device = SonyDevice(host, "SonyApiLib Python Test") device.register() pin = input("Enter the PIN displayed at your device: ") - device.send_authentication(pin) - save_device() + if device.send_authentication(pin): + save_device() + else: + print("Registration failed") + exit(1) # wake device is_on = device.get_power_status() if not is_on: device.power(True) + device.get_playing_status() apps = device.get_apps() - device.start_app(apps[0]) # Play media diff --git a/examples/simple_example.py b/examples/simple_example.py deleted file mode 100644 index 7b0a799..0000000 --- a/examples/simple_example.py +++ /dev/null @@ -1,39 +0,0 @@ -import os.path as path -import sys -from inspect import getsourcefile - -current_dir = path.dirname(path.abspath(getsourcefile(lambda: 0))) -sys.path.insert(0, current_dir[:current_dir.rfind(path.sep)]) -from sonyapilib.device import SonyDevice, AuthenticationResult -sys.path.pop(0) - - -def register_device(device): - result = device.register() - if result != AuthenticationResult.PIN_NEEDED: - print("Error in registration") - return False - - pin = input("Enter the PIN displayed at your device: ") - device.send_authentication(pin) - return True - - -if __name__ == "__main__": - - stored_config = "bluray.json" - - # device must be on for registration - host = "10.0.0.102" - device = SonyDevice(host, "SonyApiLib Python Test4") - if register_device(device): - # save_device() - apps = device.get_apps() - device.start_app(apps[0]) - - # Play media - device.play() - - - - \ No newline at end of file diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 579ff95..33ed4e5 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -583,6 +583,7 @@ def get_playing_status(self): if not content: return "OFF" return find_in_xml(content, [".//CurrentTransportState"]).text + return_value = {} resp = self._send_http(urljoin(self.base_url, "avContent"), HttpMethod.POST, json=self._create_api_json("getPlayingContentInfo")) @@ -628,7 +629,7 @@ def start_app(self, app_name): url = 'http://{}/DIAL/apps/{}'.format(self.host, self.apps[app_name].id) self._send_http(url, HttpMethod.POST) - def power(self, power_on, broadcast=None): + def power(self, power_on, broadcast='255.255.255.255'): """Powers the device on or shuts it off.""" if power_on: self.wakeonlan(broadcast) diff --git a/tests/data/playing_status_legacy_no_media.xml b/tests/data/playing_status_legacy_no_media.xml new file mode 100644 index 0000000..411c99f --- /dev/null +++ b/tests/data/playing_status_legacy_no_media.xml @@ -0,0 +1,11 @@ + + + + + NO_MEDIA_PRESENT + OK + 1 + + + \ No newline at end of file diff --git a/tests/data/playing_status_legacy_playing.xml b/tests/data/playing_status_legacy_playing.xml new file mode 100644 index 0000000..00c3485 --- /dev/null +++ b/tests/data/playing_status_legacy_playing.xml @@ -0,0 +1,11 @@ + + + + + PLAYING + OK + 1 + + + \ No newline at end of file diff --git a/tests/device_test.py b/tests/device_test.py index f65ddd7..3425707 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -7,7 +7,7 @@ urljoin ) -from requests import HTTPError +from requests import HTTPError, URLRequired from tests.testutil import read_file @@ -35,10 +35,12 @@ APP_LIST_URL = 'http://test:50202/appslist' APP_LIST_URL_V4 = 'http://test/DIAL/sony/applist' APP_START_URL_LEGACY = 'http://test:50202/apps/' +APP_START_URL = 'http://test/DIAL/apps/' SOAP_URL = 'http://test/soap' GET_REMOTE_CONTROLLER_INFO_URL = "http://test/getRemoteControllerInfo" BASE_URL = 'http://test/sony' - +AV_TRANSPORT_URL = 'http://test:52323/upnp/control/AVTransport' +AV_TRANSPORT_URL_NO_MEDIA = 'http://test2:52323/upnp/control/AVTransport' def mock_error(*args, **kwargs): raise Exception() @@ -92,7 +94,9 @@ def raise_for_status(self): def mocked_requests_post(*args, **kwargs): url = args[0] print("POST for URL: {}".format(url)) - if url == REGISTRATION_URL_V4: + if not url: + raise URLRequired() + elif url == REGISTRATION_URL_V4: return MockResponse({}, 200) elif url == REGISTRATION_URL_V4_FAIL: return MockResponse({"error": 402}, 200) @@ -105,6 +109,12 @@ def mocked_requests_post(*args, **kwargs): return MockResponse({"result": [result]}, 200) elif APP_START_URL_LEGACY in url: return MockResponse(None, 200) + elif APP_START_URL in url: + return MockResponse(None, 200) + elif url == AV_TRANSPORT_URL: + return MockResponse(None, 200, read_file('data/playing_status_legacy_playing.xml')) + elif url == AV_TRANSPORT_URL_NO_MEDIA: + return MockResponse(None, 200, read_file('data/playing_status_legacy_no_media.xml')) else: raise ValueError("Unknown url requested: {}".format(url)) @@ -316,8 +326,8 @@ def test_update_commands_v4(self, mock_get): device.actions["getRemoteControllerInfo"] = action device._update_commands() - def start_app_legacy(self, device, app_name, mock_post, mock_send_command): - versions = [1, 2, 3] + def start_app(self, device, app_name, mock_post, mock_send_command): + versions = [1, 2, 3, 4] apps = { "Video Explorer": "com.sony.videoexplorer", "Music Explorer": "com.sony.musicexplorer", @@ -348,16 +358,15 @@ def start_app_legacy(self, device, app_name, mock_post, mock_send_command): self.assertEqual(mock_post.call_count, 1) self.assertEqual(mock_send_command.call_count, 1) - url = APP_START_URL_LEGACY + apps[app_name] + if version < 4: + url = APP_START_URL_LEGACY + apps[app_name] + else: + url = APP_START_URL + apps[app_name] self.assertEqual(url, mock_post.call_args[0][0]) mock_send_command.call_count = 0 mock_post.call_count = 0 mock_post.mock_calls.clear() - def start_app_v4(self, device, app_name): - device.api_version = 4 - # todo - @mock.patch('sonyapilib.device.SonyDevice._send_command', side_effect=mock_nothing) @mock.patch('requests.post', side_effect=mocked_requests_post) @mock.patch('requests.get', side_effect=mocked_requests_get) @@ -377,10 +386,7 @@ def test_update_applist(self, mock_get, mock_post, mock_send_command): device._update_applist() for app in device.get_apps(): self.assertTrue(app in app_list) - if device.api_version < 4: - self.start_app_legacy(device, app, mock_post, mock_send_command) - else: - self.start_app_v4(device, app) + self.start_app(device, app, mock_post, mock_send_command) self.assertEqual(len(device.apps), len(app_list)) def test_recreate_authentication_no_auth(self): @@ -623,14 +629,24 @@ def test_power_off(self, mock_send_command): @mock.patch('sonyapilib.device.SonyDevice.get_power_status', side_effect=mock_nothing) @mock.patch('sonyapilib.device.SonyDevice._send_command', side_effect=mock_nothing) @mock.patch('sonyapilib.device.SonyDevice.wakeonlan', side_effect=mock_nothing) - def test_power_off(self, mock_wake_on_lan, mock_send_command, mock_get_power_status): + def test_power_on(self, mock_wake_on_lan, mock_send_command, mock_get_power_status): device = self.create_device() device.power(True) self.assertEqual(mock_send_command.call_count, 1) self.assertEqual(mock_wake_on_lan.call_count, 1) self.assertEqual(mock_send_command.mock_calls[0][1][0], "Power") - pass + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_playing_status_no_media_legacy(self, mocked_requests_post): + device = self.create_device() + self.assertEqual("OFF", device.get_playing_status()) + + device.av_transport_url = AV_TRANSPORT_URL_NO_MEDIA + device.get_playing_status() + + device.av_transport_url = AV_TRANSPORT_URL + self.assertEqual("PLAYING", device.get_playing_status()) + @staticmethod def create_command_list(device): command = XmlApiObject({}) @@ -643,8 +659,7 @@ def create_device(): return SonyDevice("test", "test") def verify_device_dmr(self, device): - self.assertEqual(device.av_transport_url, - 'http://test:52323/upnp/control/AVTransport') + self.assertEqual(device.av_transport_url, AV_TRANSPORT_URL) @staticmethod def verify_cookies(device): From e8d04cf22516260326ade9437b75628d36fecac7 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Apr 2019 16:19:56 +0200 Subject: [PATCH 108/170] updated readme. --- README.md | 69 ++++++++++++++------------------------- examples/pair_and_apps.py | 7 ++-- examples/readme.py | 16 +++++++++ 3 files changed, 45 insertions(+), 47 deletions(-) create mode 100644 examples/readme.py diff --git a/README.md b/README.md index bf7d468..fcf26c4 100644 --- a/README.md +++ b/README.md @@ -25,41 +25,20 @@ The example will load a json file with all data if it exists or connects to devi ``` from sonyapilib.device import SonyDevice -def save_device(): - data = device.save_to_json() - text_file = open("bluray.json", "w") - text_file.write(data) - text_file.close() - if __name__ == "__main__": - - stored_config = "bluray.json" - device = None - import os.path - if os.path.exists(stored_config): - with open(stored_config, 'r') as content_file: - json_data = content_file.read() - device = SonyDevice.load_from_json(json_data) - else: - # device must be on for registration - host = "10.0.0.102" - device = SonyDevice(host, "SonyApiLib Python Test") - device.register() - pin = input("Enter the PIN displayed at your device: ") - device.send_authentication(pin) - save_device() - - # wake device - is_on = device.get_power_status() - if not is_on: - device.power(True) + # device must be on for registration + host = "10.0.0.102" + device = SonyDevice(host, "SonyApiLib Python Test") + device.register() + pin = input("Enter the PIN displayed at your device: ") + if not device.send_authentication(pin): + print("Failed to register device") + exit(1) apps = device.get_apps() - device.start_app(apps[0]) - - # Play media device.play() + ``` More examples can be found in the examples folder. @@ -72,38 +51,40 @@ https://gist.github.com/kalleth/e10e8f3b8b7cb1bac21463b0073a65fb # Compatibility List -LCD TV BRAVIA -2016 model or later: -*KDL-W/WD, KLV-W Series (2016 model) are not compatible with Video & TV SideView. (Except for KDL-W800D/W950D) *You can not install Video & TV SideView app into your Sony's Android TVâ„¢. +## LCD TV BRAVIA +### 2016 model or later
+KDL-W/WD, KLV-W Series (2016 model) are not compatible with Video & TV SideView. (Except for KDL-W800D/W950D) + +You can not install Video & TV SideView app into your Sony's Android TV™. -2015 model +### 2015 model XBR-X94xC series, XBR-X93xC series, XBR-X91xC series, XBR-X90xC series, XBR-X85xC series, XBR-X83xC series, XBR-X80xC series, KD-X94xxC series, KD-X93xxC series, KD-X91xxC series, KD-X90xxC series, KD-X85xxC series, KD-X83xxC series, KD-X80xxC series, KDL-W95xC series, KDL-W85xC series, KDL-W80xC series, KDL-W75xC series, KDL-W70xC series, KDL-W600A series Please update your TV software to the latest version. For how to update the software. -You can not install Video & TV SideView app into your Sony's Android TV™. -2014 model + +### 2014 model XBR-X95xB series, XBR-X90xB series, XBR-X85xB series, KD-X95xxB series, KD-X90xxB series, KD-X85xxB series, KD-X83xxC series, KD-X80xxB series, KDL-W95xB series, KDL-W92xA series, KDL-W90xB series, KDL-W85xB series, KDL-W83xB series, KDL-W8xxB series, KDL-W7xxB series, KDL-W6xxB series, KDL-W5xxA series -2013 model +### 2013 model XBR-X90xA series, XBR-X85xA series, KD-X900xA series, KD-X850xA series, KDL-W95xA series, KDL-W90xA series, KDL-W85xA series, KDL-W80xA series, KDL-W70xA series, KDL-W67xA series, KDL-W65xA series, KDL-W60xA series, KDL-S99xA series -2012 model +### 2012 model XBR-X90x series, KD-X900x series, XBR-HX95 series, KDL-HX95 series, KDL-HX85 series, KDL-HX75 series, KDL-NX65 series, KDL-EX75 series, KDL-EX65 series, KDL-EX55 series, KDL-EX54 series -2011 model +### 2011 model XBR-HX92 series, KDL-HX92 series, KDL-HX82 series, KDL-HX72 series, KDL-NX72 series, KDL-EX72 series, KDL-EX62 series, KDL-EX52 series, KDL-EX42 series, KDL-EX32 series, KDL-CX52 series, KDL-CX40 series -Blu-ray Disc™/DVD Player +### Blu-ray Disc™/DVD Player UHP-H1, BDP-S6700, BDP-S3700, BDP-S6500, BDP-S5500, BDP-S4500, BDP-S3500, BDP-S7200, BDP-S6200, BDP-S5200, BDP-S4200, BDP-S3200, BDP-BX620, BDP-BX520, BDP-BX320, BDP-S5100, BDP-S4100, BDP-S3100, BDP-BX510, BDP-BX310, BDP-A6000, BDP-S790, BDP-S590, BDP-S490, BDP-S390, BDP-BX59, BDP-BX39, BDP-S780, BDP-S580, BDP-S480, BDP-S380, BDP-BX58, BDP-BX38, BDP-S1700ES, BDP-S770, BDP-S570, BDP-S470, BDP-S370, BDP-BX57, BDP-BX37 -Blu-ray Disc™/DVD Home Theatre System +### Blu-ray Disc™/DVD Home Theatre System BDV-N9200WL, BDV-N9200W, BDV-NF7220, BDV-N7200WL, BDV-N7200W, BDV-N5200W, BDV-E3200, BDV-N9900SH, BDV-N9150WL, BDV-N9150W, BDV-N9100WL, BDV-N9100W , BDV-N8100WL, BDV-N8100W , BDV-N7100WL, BDV-N7100W, BDV-E6100 , BDV-E5100, BDV-E4100, BDV-E3100, BDV-E2100, BDV-EF1100, BDV-N995W, BDV-N990W, BDV-N890W, BDV-N790W, BDV-N590, BDV-E690, BDV-E490, BDV-E390, BDV-E385, BDV-E290, BDV-E190, BDV-NF720, BDV-NF620, BDV-EF420, BDV-EF220, BDV-T79, BDV-T39, BDV-E985W, BDV-E980W, BDV-E980, BDV-E880, BDV-E780W, BDV-E580, BDV-E380, BDV-L800M, BDV-L800, BDV-L600, BDV-T58, BDV-IZ1000W, BDV-HZ970W, BDV-E970W, BDV-E870, BDV-E770W, BDV-E670W, BDV-E570, BDV-E470, BDV-E370, BDV-F700, BDV-F500, BDV-F7, BDV-T57 -Streaming Player / Network Media Player +### Streaming Player / Network Media Player NSZ-GS8, NSZ-GU1, NSZ-GX70, NSZ-GS7, SMP-N200, SMP-N100 -Sony Internet TV +### Sony Internet TV NSX-46GT1, NSX-40GT1, NSX-32GT1, NSX-24GT1, NSZ-GT1 -AV Receiver +### AV Receiver STR-DN1070, STR-DN1060, STR-DN860, STR-DN1050, STR-DN850, STR-DN1040, STR-DN840, STR-DA1800ES, STR-DN1030, STR-DN1020 diff --git a/examples/pair_and_apps.py b/examples/pair_and_apps.py index aeb19f5..b9326e6 100644 --- a/examples/pair_and_apps.py +++ b/examples/pair_and_apps.py @@ -2,6 +2,7 @@ CONFIG_FILE = "bluray.json" + def save_device(): data = device.save_to_json() text_file = open(CONFIG_FILE, "w") @@ -11,12 +12,12 @@ def save_device(): def load_device(): import os - device = None + sony_device = None if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, 'r') as content_file: json_data = content_file.read() - device = SonyDevice.load_from_json(json_data) - return device + sony_device = SonyDevice.load_from_json(json_data) + return sony_device if __name__ == "__main__": diff --git a/examples/readme.py b/examples/readme.py new file mode 100644 index 0000000..a720f68 --- /dev/null +++ b/examples/readme.py @@ -0,0 +1,16 @@ +"""Example found in the readme""" +from sonyapilib.device import SonyDevice + +if __name__ == "__main__": + # device must be on for registration + host = "10.0.0.102" + device = SonyDevice(host, "SonyApiLib Python Test") + device.register() + pin = input("Enter the PIN displayed at your device: ") + if not device.send_authentication(pin): + print("Failed to register device") + exit(1) + + apps = device.get_apps() + device.start_app(apps[0]) + device.play() From 51df6eb961ea39a9f7922af2e46f9caa401cfbb5 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 9 Apr 2019 17:40:01 +0200 Subject: [PATCH 109/170] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fcf26c4..de5dd8c 100644 --- a/README.md +++ b/README.md @@ -51,8 +51,7 @@ https://gist.github.com/kalleth/e10e8f3b8b7cb1bac21463b0073a65fb # Compatibility List -## LCD TV BRAVIA -### 2016 model or later
+### 2016 model or later KDL-W/WD, KLV-W Series (2016 model) are not compatible with Video & TV SideView. (Except for KDL-W800D/W950D) You can not install Video & TV SideView app into your Sony's Android TVâ„¢. @@ -60,7 +59,7 @@ You can not install Video & TV SideView app into your Sony's Android TVâ„¢. ### 2015 model XBR-X94xC series, XBR-X93xC series, XBR-X91xC series, XBR-X90xC series, XBR-X85xC series, XBR-X83xC series, XBR-X80xC series, KD-X94xxC series, KD-X93xxC series, KD-X91xxC series, KD-X90xxC series, KD-X85xxC series, KD-X83xxC series, KD-X80xxC series, KDL-W95xC series, KDL-W85xC series, KDL-W80xC series, KDL-W75xC series, KDL-W70xC series, KDL-W600A series -Please update your TV software to the latest version. For how to update the software. +Please update your TV software to the latest version. ### 2014 model XBR-X95xB series, XBR-X90xB series, XBR-X85xB series, KD-X95xxB series, KD-X90xxB series, KD-X85xxB series, KD-X83xxC series, KD-X80xxB series, KDL-W95xB series, KDL-W92xA series, KDL-W90xB series, KDL-W85xB series, KDL-W83xB series, KDL-W8xxB series, KDL-W7xxB series, KDL-W6xxB series, KDL-W5xxA series From 24e1fc5a499a3ac1265d7ff51099454bec411baf Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 11 Apr 2019 11:39:30 +0200 Subject: [PATCH 110/170] Update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index de5dd8c..86a1644 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ pip install sonyapilib This library has been tested with python 3.5 and above, functionality for older python version cannot be guaranteed. # Example -The example will load a json file with all data if it exists or connects to device and registers it, storing the json afterwards +This example connections with device (of which the ip address is hard coded) start the registration process and starts the first available. More detailed examples can be found in the examples folder. ``` from sonyapilib.device import SonyDevice @@ -41,8 +41,6 @@ if __name__ == "__main__": ``` -More examples can be found in the examples folder. - # URL list https://github.com/chr15m/media-remote/blob/master/SNIFF.md From b53cd83cebca860f53bc99c34e32761e12a84606 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 14 Apr 2019 17:32:36 +0200 Subject: [PATCH 111/170] Finished v4 impl. --- examples/pair_and_apps.py | 14 +- sonyapilib/device.py | 96 ++++----- tests/data/commandList.json | 403 ++++++++++++++++++++++++++++++++++++ tests/data/cookies.json | 50 +++++ tests/data/dmr_v4.xml | 8 +- tests/device_test.py | 82 +++++--- 6 files changed, 562 insertions(+), 91 deletions(-) create mode 100644 tests/data/commandList.json create mode 100644 tests/data/cookies.json diff --git a/examples/pair_and_apps.py b/examples/pair_and_apps.py index b9326e6..e4e4603 100644 --- a/examples/pair_and_apps.py +++ b/examples/pair_and_apps.py @@ -24,8 +24,8 @@ def load_device(): device = load_device() if not device: # device must be on for registration - host = "10.0.0.102" - device = SonyDevice(host, "SonyApiLib Python Test") + host = "192.168.178.23" + device = SonyDevice(host, "Test123") device.register() pin = input("Enter the PIN displayed at your device: ") if device.send_authentication(pin): @@ -39,9 +39,13 @@ def load_device(): if not is_on: device.power(True) - device.get_playing_status() - apps = device.get_apps() - device.start_app(apps[0]) + status = device.get_playing_status() + apps = device.get_apps() + device.pause() + for app in device.apps: + if "youtube" in app.lower(): + device.start_app(app) + device.get_playing_status() # Play media device.play() diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 33ed4e5..7c2dd21 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -70,8 +70,6 @@ class SonyDevice: # pylint: disable=too-many-public-methods # pylint: disable=too-many-instance-attributes # pylint: disable=fixme - # todo remove this again. - # todo check if commands, especially soap does work with v4. """Contains all data for the device.""" def __init__(self, host, nickname): @@ -109,7 +107,8 @@ def __init__(self, host, nickname): self.app_url = "http://{0.host}:{0.app_port}".format(self) self.base_url = "http://{0.host}/sony/".format(self) - def _init_device(self): + def init_device(self): + """Update this object with data from the device""" self._update_service_urls() if self.pin: @@ -120,8 +119,6 @@ def _init_device(self): @staticmethod def discover(): """Discover all available devices.""" - - # Todo check if this works with v4 discovery = ssdp.SSDPDiscovery() devices = [] for device in discovery.discover( @@ -135,7 +132,9 @@ def discover(): @staticmethod def load_from_json(data): """Load a device configuration from a stored json.""" - return jsonpickle.decode(data) + device = jsonpickle.decode(data) + device.init_device() + return device def save_to_json(self): """Save this device configuration into a json.""" @@ -150,10 +149,7 @@ def _update_service_urls(self): try: self._parse_dmr(response.text) - if self.api_version > 3: - # todo implement this - pass - else: + if self.api_version < 3: self._parse_ircc() self._parse_action_list() self._parse_system_information() @@ -289,13 +285,13 @@ def _update_commands(self): _LOGGER.error("Registration necessary to read command list.") return - if self.api_version > 3: - self._parse_command_list_v4() - else: + if self.api_version < 3: self._parse_command_list() + else: + self._parse_command_list_v4() def _parse_command_list_v4(self): - action_name = "getRemoteControllerInfo" + action_name = "getRemoteCommandList" action = self.actions[action_name] json_data = self._create_api_json(action.value) @@ -304,13 +300,16 @@ def _parse_command_list_v4(self): ) if not response: - _LOGGER.error("no response received") + _LOGGER.debug("no response received, device might be off") return json_resp = response.json() if json_resp and not json_resp.get('error'): - # todo parse this into the old structure. - self.commands = json_resp.get('result')[1] + for command in json_resp.get('result')[1]: + api_object = XmlApiObject(command) + if api_object.name == "PowerOff": + api_object.name = "Power" + self.commands[api_object.name] = api_object else: _LOGGER.error("JSON request error: %s", json.dumps(json_resp, indent=4)) @@ -320,7 +319,7 @@ def _parse_command_list(self): url = self._get_action("getRemoteCommandList").url response = self._send_http(url, method=HttpMethod.GET) if not response: - _LOGGER.error("Failed to get response for command list") + _LOGGER.debug("Failed to get response for command list, device might be off") return for command in find_in_xml(response.text, [("command", True)]): @@ -331,10 +330,12 @@ def _update_applist(self): """Update the list of apps which are supported by the device.""" if self.api_version < 4: url = self.app_url + "/appslist" + response = self._send_http(url, method=HttpMethod.GET) else: url = 'http://{}/DIAL/sony/applist'.format(self.host) + response = self._send_http( + url, method=HttpMethod.GET, cookies=self._recreate_auth_cookie()) - response = self._send_http(url, method=HttpMethod.GET) if response: for app in find_in_xml(response.text, [(".//app", True)]): data = XmlApiObject({ @@ -345,11 +346,6 @@ def _update_applist(self): def _recreate_authentication(self): """The default cookie is for URL/sony. For some commands we need it for the root path.""" - - # todo fix cookies - # cookies = None - # cookies = requests.cookies.RequestsCookieJar() - # cookies.set("auth", self.cookies.get("auth")) registration_action = self._get_action("register") if any([not registration_action, registration_action.mode < 3]): return @@ -447,7 +443,7 @@ def _send_req_ircc(self, params): def _send_command(self, name): if not self.commands: - self._init_device() + self.init_device() if self.commands: if name in self.commands: @@ -460,7 +456,7 @@ def _send_command(self, name): def _get_action(self, name): """Get the action object for the action with the given name""" if name not in self.actions and not self.actions: - self._init_device() + self.init_device() if name not in self.actions and not self.actions: raise ValueError('Failed to read action list from device.') @@ -504,7 +500,8 @@ def _register_v4(self, registration_action): } response = self._send_http(registration_action.url, method=HttpMethod.POST, headers=headers, - json=authorization, raise_errors=True) + auth=('', self.pin), + data=json.dumps(authorization), raise_errors=True) except requests.exceptions.RequestException as ex: return self._handle_register_error(ex) @@ -516,6 +513,15 @@ def _register_v4(self, registration_action): self.cookies = response.cookies return AuthenticationResult.SUCCESS + def _recreate_auth_cookie(self): + """ + The default cookie is for URL/sony. + For some commands we need it for the root path + """ + cookies = requests.cookies.RequestsCookieJar() + cookies.set("auth", self.cookies.get("auth")) + return cookies + def get_device_id(self): """Returns the id which is used for the registration.""" return "TVSideView:{0}".format(self.uuid) @@ -543,7 +549,7 @@ def register(self): "Registration mode {0} is not supported".format(registration_action.mode)) if registration_result is AuthenticationResult.SUCCESS: - self._init_device() + self.init_device() return registration_result @@ -571,27 +577,17 @@ def wakeonlan(self, broadcast='255.255.255.255'): def get_playing_status(self): """Get the status of playback from the device""" - if self.api_version < 4: - data = """ - 0 - """ - - action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" - - content = self._post_soap_request( - url=self.av_transport_url, params=data, action=action) - if not content: - return "OFF" - return find_in_xml(content, [".//CurrentTransportState"]).text - - return_value = {} - resp = self._send_http(urljoin(self.base_url, "avContent"), HttpMethod.POST, - json=self._create_api_json("getPlayingContentInfo")) - if resp is not None and not resp.get('error'): - playing = resp.get('result')[0] - # todo get the playing status and return it. - return_value['programTitle'] = playing.get('programTitle') - return return_value + data = """ + 0 + """ + + action = "urn:schemas-upnp-org:service:AVTransport:1#GetTransportInfo" + + content = self._post_soap_request( + url=self.av_transport_url, params=data, action=action) + if not content: + return "OFF" + return find_in_xml(content, [".//CurrentTransportState"]).text def get_power_status(self): """Checks if the device is online.""" @@ -627,7 +623,7 @@ def start_app(self, app_name): self._send_http(url, HttpMethod.POST, data=data) else: url = 'http://{}/DIAL/apps/{}'.format(self.host, self.apps[app_name].id) - self._send_http(url, HttpMethod.POST) + self._send_http(url, HttpMethod.POST, cookies=self._recreate_auth_cookie()) def power(self, power_on, broadcast='255.255.255.255'): """Powers the device on or shuts it off.""" diff --git a/tests/data/commandList.json b/tests/data/commandList.json new file mode 100644 index 0000000..d43afcf --- /dev/null +++ b/tests/data/commandList.json @@ -0,0 +1,403 @@ +{ + "id": 1, + "result": [ + { + "bundled": true, + "type": "RM-J1100" + }, + [ + { + "name": "PowerOff", + "value": "AAAAAQAAAAEAAAAvAw==" + }, + { + "name": "Input", + "value": "AAAAAQAAAAEAAAAlAw==" + }, + { + "name": "GGuide", + "value": "AAAAAQAAAAEAAAAOAw==" + }, + { + "name": "EPG", + "value": "AAAAAgAAAKQAAABbAw==" + }, + { + "name": "Favorites", + "value": "AAAAAgAAAHcAAAB2Aw==" + }, + { + "name": "Display", + "value": "AAAAAQAAAAEAAAA6Aw==" + }, + { + "name": "Home", + "value": "AAAAAQAAAAEAAABgAw==" + }, + { + "name": "Options", + "value": "AAAAAgAAAJcAAAA2Aw==" + }, + { + "name": "Return", + "value": "AAAAAgAAAJcAAAAjAw==" + }, + { + "name": "Up", + "value": "AAAAAQAAAAEAAAB0Aw==" + }, + { + "name": "Down", + "value": "AAAAAQAAAAEAAAB1Aw==" + }, + { + "name": "Right", + "value": "AAAAAQAAAAEAAAAzAw==" + }, + { + "name": "Left", + "value": "AAAAAQAAAAEAAAA0Aw==" + }, + { + "name": "Confirm", + "value": "AAAAAQAAAAEAAABlAw==" + }, + { + "name": "Red", + "value": "AAAAAgAAAJcAAAAlAw==" + }, + { + "name": "Green", + "value": "AAAAAgAAAJcAAAAmAw==" + }, + { + "name": "Yellow", + "value": "AAAAAgAAAJcAAAAnAw==" + }, + { + "name": "Blue", + "value": "AAAAAgAAAJcAAAAkAw==" + }, + { + "name": "Num1", + "value": "AAAAAQAAAAEAAAAAAw==" + }, + { + "name": "Num2", + "value": "AAAAAQAAAAEAAAABAw==" + }, + { + "name": "Num3", + "value": "AAAAAQAAAAEAAAACAw==" + }, + { + "name": "Num4", + "value": "AAAAAQAAAAEAAAADAw==" + }, + { + "name": "Num5", + "value": "AAAAAQAAAAEAAAAEAw==" + }, + { + "name": "Num6", + "value": "AAAAAQAAAAEAAAAFAw==" + }, + { + "name": "Num7", + "value": "AAAAAQAAAAEAAAAGAw==" + }, + { + "name": "Num8", + "value": "AAAAAQAAAAEAAAAHAw==" + }, + { + "name": "Num9", + "value": "AAAAAQAAAAEAAAAIAw==" + }, + { + "name": "Num0", + "value": "AAAAAQAAAAEAAAAJAw==" + }, + { + "name": "Num11", + "value": "AAAAAQAAAAEAAAAKAw==" + }, + { + "name": "Num12", + "value": "AAAAAQAAAAEAAAALAw==" + }, + { + "name": "VolumeUp", + "value": "AAAAAQAAAAEAAAASAw==" + }, + { + "name": "VolumeDown", + "value": "AAAAAQAAAAEAAAATAw==" + }, + { + "name": "Mute", + "value": "AAAAAQAAAAEAAAAUAw==" + }, + { + "name": "ChannelUp", + "value": "AAAAAQAAAAEAAAAQAw==" + }, + { + "name": "ChannelDown", + "value": "AAAAAQAAAAEAAAARAw==" + }, + { + "name": "SubTitle", + "value": "AAAAAgAAAJcAAAAoAw==" + }, + { + "name": "ClosedCaption", + "value": "AAAAAgAAAKQAAAAQAw==" + }, + { + "name": "Enter", + "value": "AAAAAQAAAAEAAAALAw==" + }, + { + "name": "DOT", + "value": "AAAAAgAAAJcAAAAdAw==" + }, + { + "name": "Analog", + "value": "AAAAAgAAAHcAAAANAw==" + }, + { + "name": "Teletext", + "value": "AAAAAQAAAAEAAAA/Aw==" + }, + { + "name": "Exit", + "value": "AAAAAQAAAAEAAABjAw==" + }, + { + "name": "Analog2", + "value": "AAAAAQAAAAEAAAA4Aw==" + }, + { + "name": "*AD", + "value": "AAAAAgAAABoAAAA7Aw==" + }, + { + "name": "Digital", + "value": "AAAAAgAAAJcAAAAyAw==" + }, + { + "name": "Analog?", + "value": "AAAAAgAAAJcAAAAuAw==" + }, + { + "name": "BS", + "value": "AAAAAgAAAJcAAAAsAw==" + }, + { + "name": "CS", + "value": "AAAAAgAAAJcAAAArAw==" + }, + { + "name": "BSCS", + "value": "AAAAAgAAAJcAAAAQAw==" + }, + { + "name": "Ddata", + "value": "AAAAAgAAAJcAAAAVAw==" + }, + { + "name": "PicOff", + "value": "AAAAAQAAAAEAAAA+Aw==" + }, + { + "name": "Tv_Radio", + "value": "AAAAAgAAABoAAABXAw==" + }, + { + "name": "Theater", + "value": "AAAAAgAAAHcAAABgAw==" + }, + { + "name": "SEN", + "value": "AAAAAgAAABoAAAB9Aw==" + }, + { + "name": "InternetWidgets", + "value": "AAAAAgAAABoAAAB6Aw==" + }, + { + "name": "InternetVideo", + "value": "AAAAAgAAABoAAAB5Aw==" + }, + { + "name": "Netflix", + "value": "AAAAAgAAABoAAAB8Aw==" + }, + { + "name": "SceneSelect", + "value": "AAAAAgAAABoAAAB4Aw==" + }, + { + "name": "Mode3D", + "value": "AAAAAgAAAHcAAABNAw==" + }, + { + "name": "iManual", + "value": "AAAAAgAAABoAAAB7Aw==" + }, + { + "name": "Audio", + "value": "AAAAAQAAAAEAAAAXAw==" + }, + { + "name": "Wide", + "value": "AAAAAgAAAKQAAAA9Aw==" + }, + { + "name": "Jump", + "value": "AAAAAQAAAAEAAAA7Aw==" + }, + { + "name": "PAP", + "value": "AAAAAgAAAKQAAAB3Aw==" + }, + { + "name": "MyEPG", + "value": "AAAAAgAAAHcAAABrAw==" + }, + { + "name": "ProgramDescription", + "value": "AAAAAgAAAJcAAAAWAw==" + }, + { + "name": "WriteChapter", + "value": "AAAAAgAAAHcAAABsAw==" + }, + { + "name": "TrackID", + "value": "AAAAAgAAABoAAAB+Aw==" + }, + { + "name": "TenKey", + "value": "AAAAAgAAAJcAAAAMAw==" + }, + { + "name": "AppliCast", + "value": "AAAAAgAAABoAAABvAw==" + }, + { + "name": "acTVila", + "value": "AAAAAgAAABoAAAByAw==" + }, + { + "name": "DeleteVideo", + "value": "AAAAAgAAAHcAAAAfAw==" + }, + { + "name": "PhotoFrame", + "value": "AAAAAgAAABoAAABVAw==" + }, + { + "name": "TvPause", + "value": "AAAAAgAAABoAAABnAw==" + }, + { + "name": "KeyPad", + "value": "AAAAAgAAABoAAAB1Aw==" + }, + { + "name": "Media", + "value": "AAAAAgAAAJcAAAA4Aw==" + }, + { + "name": "SyncMenu", + "value": "AAAAAgAAABoAAABYAw==" + }, + { + "name": "Forward", + "value": "AAAAAgAAAJcAAAAcAw==" + }, + { + "name": "Play", + "value": "AAAAAgAAAJcAAAAaAw==" + }, + { + "name": "Rewind", + "value": "AAAAAgAAAJcAAAAbAw==" + }, + { + "name": "Prev", + "value": "AAAAAgAAAJcAAAA8Aw==" + }, + { + "name": "Stop", + "value": "AAAAAgAAAJcAAAAYAw==" + }, + { + "name": "Next", + "value": "AAAAAgAAAJcAAAA9Aw==" + }, + { + "name": "Rec", + "value": "AAAAAgAAAJcAAAAgAw==" + }, + { + "name": "Pause", + "value": "AAAAAgAAAJcAAAAZAw==" + }, + { + "name": "Eject", + "value": "AAAAAgAAAJcAAABIAw==" + }, + { + "name": "FlashPlus", + "value": "AAAAAgAAAJcAAAB4Aw==" + }, + { + "name": "FlashMinus", + "value": "AAAAAgAAAJcAAAB5Aw==" + }, + { + "name": "TopMenu", + "value": "AAAAAgAAABoAAABgAw==" + }, + { + "name": "PopUpMenu", + "value": "AAAAAgAAABoAAABhAw==" + }, + { + "name": "RakurakuStart", + "value": "AAAAAgAAAHcAAABqAw==" + }, + { + "name": "OneTouchTimeRec", + "value": "AAAAAgAAABoAAABkAw==" + }, + { + "name": "OneTouchView", + "value": "AAAAAgAAABoAAABlAw==" + }, + { + "name": "OneTouchRec", + "value": "AAAAAgAAABoAAABiAw==" + }, + { + "name": "OneTouchStop", + "value": "AAAAAgAAABoAAABjAw==" + }, + { + "name": "DUX", + "value": "AAAAAgAAABoAAABzAw==" + }, + { + "name": "FootballMode", + "value": "AAAAAgAAABoAAAB2Aw==" + }, + { + "name": "Social", + "value": "AAAAAgAAABoAAAB0Aw==" + } + ] + ] +} \ No newline at end of file diff --git a/tests/data/cookies.json b/tests/data/cookies.json new file mode 100644 index 0000000..402fd39 --- /dev/null +++ b/tests/data/cookies.json @@ -0,0 +1,50 @@ +{ + "py/object": "requests.cookies.RequestsCookieJar", + "py/state": { + "_cookies": { + "192.168.170.23": { + "/sony/": { + "auth": { + "py/object": "http.cookiejar.Cookie", + "_rest": {}, + "comment": null, + "comment_url": null, + "discard": false, + "domain": "192.168.170.23", + "domain_initial_dot": false, + "domain_specified": false, + "expires": 1556462645, + "name": "auth", + "path": "/sony/", + "path_specified": true, + "port": null, + "port_specified": false, + "rfc2109": false, + "secure": false, + "value": "b2d0ff57a51b475cb3d1a9b7516f366ed2b4ffe0164cd0fd560ff6ceb28b101a", + "version": 0 + } + } + } + }, + "_now": 1555253045, + "_policy": { + "py/object": "http.cookiejar.DefaultCookiePolicy", + "_allowed_domains": null, + "_blocked_domains": { + "py/tuple": [] + }, + "_now": 1555253045, + "hide_cookie2": false, + "netscape": true, + "rfc2109_as_netscape": null, + "rfc2965": false, + "strict_domain": false, + "strict_ns_domain": 0, + "strict_ns_set_initial_dollar": false, + "strict_ns_set_path": false, + "strict_ns_unverifiable": false, + "strict_rfc2965_unverifiable": true + } + } +} \ No newline at end of file diff --git a/tests/data/dmr_v4.xml b/tests/data/dmr_v4.xml index 6ce8337..fc8101e 100644 --- a/tests/data/dmr_v4.xml +++ b/tests/data/dmr_v4.xml @@ -107,7 +107,7 @@ urn:schemas-sony-com:service:IRCC:1 urn:schemas-sony-com:serviceId:IRCC /IRCCSCPD.xml - http://192.168.178.23/sony/IRCC + http://192.168.170.23/sony/IRCC @@ -131,11 +131,11 @@ VEN_0106&DEV_0007&REV_01 Display.TV Multimedia.DMR - http://192.168.178.23/sony/BgmSearch + http://192.168.170.23/sony/BgmSearch 1.0 - http://192.168.178.23/sony + http://192.168.170.23/sony guide system @@ -157,7 +157,7 @@ 20677 - http://192.168.178.23/DIAL/sony/applist + http://192.168.170.23/DIAL/sony/applist B0:00:03:3F:5A:DE CoreTV_DIAL diff --git a/tests/device_test.py b/tests/device_test.py index 3425707..f467f35 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -7,6 +7,7 @@ urljoin ) +import jsonpickle from requests import HTTPError, URLRequired from tests.testutil import read_file @@ -28,10 +29,11 @@ SYSTEM_INFORMATION_URL = 'http://192.168.240.4:50002/getSystemInformation' GET_REMOTE_COMMAND_LIST_URL = 'http://192.168.240.4:50002/getRemoteCommandList' REGISTRATION_URL_LEGACY = 'http://192.168.240.4:50002/register' -REGISTRATION_URL_V4 = 'http://192.168.178.23/sony/accessControl' -REGISTRATION_URL_V4_FAIL = 'http://192.168.178.22/sony/accessControl' -REGISTRATION_URL_V4_FAIL_401 = 'http://192.168.178.25/sony/accessControl' +REGISTRATION_URL_V4 = 'http://192.168.170.23/sony/accessControl' +REGISTRATION_URL_V4_FAIL = 'http://192.168.170.22/sony/accessControl' +REGISTRATION_URL_V4_FAIL_401 = 'http://192.168.170.25/sony/accessControl' REGISTRATION_URL_V3_FAIL_401 = 'http://192.168.240.7:50002/register' +COMMAND_LIST_V4 = 'http://192.168.240.4:50002/getRemoteCommandList' APP_LIST_URL = 'http://test:50202/appslist' APP_LIST_URL_V4 = 'http://test/DIAL/sony/applist' APP_START_URL_LEGACY = 'http://test:50202/apps/' @@ -71,6 +73,7 @@ def get(self, key): return self.data[key] return None + class MockResponse: def __init__(self, json_data, status_code, text=None, cookies=None): self.json_obj = MockResponseJson(json_data) @@ -115,6 +118,9 @@ def mocked_requests_post(*args, **kwargs): return MockResponse(None, 200, read_file('data/playing_status_legacy_playing.xml')) elif url == AV_TRANSPORT_URL_NO_MEDIA: return MockResponse(None, 200, read_file('data/playing_status_legacy_no_media.xml')) + elif url == COMMAND_LIST_V4: + json_data = jsonpickle.decode(read_file('data/commandList.json')) + return MockResponse(json_data, 200, "") else: raise ValueError("Unknown url requested: {}".format(url)) @@ -132,7 +138,7 @@ def mocked_requests_get(*args, **kwargs): return MockResponse(None, 200, read_file("data/getSysteminformation.xml")) elif url == GET_REMOTE_COMMAND_LIST_URL: return MockResponse(None, 200, read_file("data/getRemoteCommandList.xml")) - elif url == APP_LIST_URL or url == APP_LIST_URL_V4: # todo make sure lists are equal + elif url == APP_LIST_URL or url == APP_LIST_URL_V4: return MockResponse(None, 200, read_file("data/appsList.xml")) elif url == REGISTRATION_URL_LEGACY: return MockResponse({}, 200) @@ -155,7 +161,7 @@ class SonyDeviceTest(unittest.TestCase): def test_init_device_no_pin(self, mock_update_applist, mock_update_command, mock_recreate_auth, mock_update_service_url): device = self.create_device() - device._init_device() + device.init_device() self.assertEqual(mock_update_service_url.call_count, 1) self.assertEqual(mock_recreate_auth.call_count, 0) self.assertEqual(mock_update_command.call_count, 0) @@ -169,7 +175,7 @@ def test_init_device_with_pin(self, mock_update_applist, mock_update_command, mock_recreate_auth, mock_update_service_url): device = self.create_device() device.pin = 1234 - device._init_device() + device.init_device() self.assertEqual(mock_update_service_url.call_count, 1) self.assertEqual(mock_recreate_auth.call_count, 1) self.assertEqual(mock_update_command.call_count, 1) @@ -200,10 +206,6 @@ def test_update_service_urls_error_processing(self, mock_error, mocked_requests_ device._update_service_urls() self.assertEqual(mock_error.call_count, 1) - def test_update_service_urls_v4(self): - # todo - pass - @mock.patch('requests.get', side_effect=mocked_requests_get) @mock.patch('sonyapilib.device.SonyDevice._parse_ircc', side_effect=mock_nothing) @mock.patch('sonyapilib.device.SonyDevice._parse_action_list', side_effect=mock_nothing) @@ -236,7 +238,7 @@ def test_parse_dmr_v4(self, mock_get): device.actions["register"].url, REGISTRATION_URL_V4) self.assertEqual(device.actions["register"].mode, 4) self.assertEqual( - device.actions["getRemoteCommandList"].url, 'http://192.168.178.23/sony/system') + device.actions["getRemoteCommandList"].url, 'http://192.168.170.23/sony/system') def test_parse_ircc_error(self): device = self.create_device() @@ -294,14 +296,30 @@ def prepare_test_action_list(self): return device def test_parse_command_list_error(self): - device = self.prepare_test_action_list() - device._parse_command_list() + versions = [1, 2, 3, 4] + for version in versions: + device = self.prepare_test_action_list() + device.api_version = version + device._parse_command_list() @mock.patch('requests.get', side_effect=mocked_requests_get) - def test_parse_command_list(self, mock_get): - device = self.prepare_test_action_list() - device._parse_command_list() - self.assertEqual(len(device.commands), 48) + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_parse_command_list(self, mock_get, mock_post): + versions = [1, 2, 3, 4] + for version in versions: + device = self.prepare_test_action_list() + device.version = version + if version < 4: + cmd_length = 48 + device._parse_command_list() + else: + cmd_length = 98 + device._parse_command_list_v4() + self.assertTrue("Power" in device.commands) + self.assertTrue("Left" in device.commands) + self.assertTrue("Pause" in device.commands) + self.assertTrue("Num3" in device.commands) + self.assertEqual(len(device.commands), cmd_length) @mock.patch('sonyapilib.device.SonyDevice._parse_command_list', side_effect=mock_nothing) def test_update_commands_no_pin(self, mock_parse_cmd_list): @@ -323,7 +341,7 @@ def test_update_commands_v4(self, mock_get): device.api_version = 4 action = XmlApiObject({}) action.url = GET_REMOTE_CONTROLLER_INFO_URL - device.actions["getRemoteControllerInfo"] = action + device.actions["getRemoteCommandList"] = action device._update_commands() def start_app(self, device, app_name, mock_post, mock_send_command): @@ -414,7 +432,7 @@ def test_recreate_authentication_v4(self): self.assertEqual(device.headers["Authorization"], "Basic OjEyMzQ=") self.assertEqual(device.headers["Connection"], "keep-alive") - self.verify_cookies(device.cookies) + self.verify_cookies(device) def test_recreate_authentication_v4_psk(self): # todo implement psk @@ -427,7 +445,7 @@ def test_register_no_auth(self, mocked_get): result = self.register_with_version(version) self.assertEqual(result[0], AuthenticationResult.SUCCESS) - @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_register_not_supported(self, mocked_get, mocked_init_device): with self.assertRaises(ValueError): @@ -439,7 +457,7 @@ def verify_register_fail(self, version, auth_result, mocked_init_device, url=Non self.assertEqual(result[0], auth_result) self.assertEqual(mocked_init_device.call_count, 0) - @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) def test_register_fail_http_timeout(self, mocked_init_device): versions = [1, 2, 3, 4] for version in versions: @@ -447,32 +465,31 @@ def test_register_fail_http_timeout(self, mocked_init_device): @mock.patch('requests.get', side_effect=mocked_requests_get) @mock.patch('requests.post', side_effect=mocked_requests_post) - @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) def test_register_fail_pin_needed(self, mocked_init_device, mock_request_get_401, mock_request_post_401): self.verify_register_fail(3, AuthenticationResult.PIN_NEEDED, mocked_init_device, REGISTRATION_URL_V3_FAIL_401) self.verify_register_fail(4, AuthenticationResult.PIN_NEEDED, mocked_init_device, REGISTRATION_URL_V4_FAIL_401) - @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_register_success_v3(self, mocked_requests_get, mocked_init_device): result = self.register_with_version(3) self.assertEqual(result[0], AuthenticationResult.SUCCESS) self.assertEqual(mocked_init_device.call_count, 1) - @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) @mock.patch('requests.post', side_effect=mocked_requests_post) def test_register_no_json_v4(self, mocked_requests_post, mocked_init_device): result = self.register_with_version(4, REGISTRATION_URL_V4_FAIL) self.assertEqual(result[0], AuthenticationResult.ERROR) self.assertEqual(mocked_init_device.call_count, 0) - @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) @mock.patch('requests.post', side_effect=mocked_requests_post) def test_register_success_v4(self, mocked_requests_post, mocked_init_device): result = self.register_with_version(4, REGISTRATION_URL_V4) self.assertEqual(result[0], AuthenticationResult.SUCCESS) self.assertEqual(mocked_init_device.call_count, 1) - self.verify_cookies(result[1].cookies) @mock.patch('sonyapilib.device.SonyDevice.register', side_effect=mock_nothing) @mock.patch('sonyapilib.device.SonyDevice._recreate_authentication', side_effect=mock_nothing) @@ -567,7 +584,7 @@ def test_post_soap_request(self, mocked_requests_post): self.assertEqual(mock_call["data"], data) self.assertEqual(mocked_requests_post.call_count, 1) - @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) def test_get_action(self, mock_init_device): device = self.create_device() action = XmlApiObject({}) @@ -579,7 +596,7 @@ def test_get_action(self, mock_init_device): self.assertEqual(device._get_action(action.name), action) @mock.patch('sonyapilib.device.SonyDevice._send_req_ircc', side_effect=mock_nothing) - @mock.patch('sonyapilib.device.SonyDevice._init_device', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) def test_send_command_error(self, mock_init_device, mock_send_req_ircc): device = self.create_device() with self.assertRaises(ValueError): @@ -656,14 +673,15 @@ def create_command_list(device): @staticmethod def create_device(): sonyapilib.device.TIMEOUT = 0.1 - return SonyDevice("test", "test") + device = SonyDevice("test", "test") + device.cookies = jsonpickle.decode(read_file("data/cookies.json")) + return device def verify_device_dmr(self, device): self.assertEqual(device.av_transport_url, AV_TRANSPORT_URL) - @staticmethod - def verify_cookies(device): - pass # todo implement cookie verification + def verify_cookies(self, device): + self.assertTrue(device.cookies is not None) if __name__ == '__main__': From 86aa47a5b0d716ac073f7e308d0c91aae2348c41 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 14 Apr 2019 17:41:39 +0200 Subject: [PATCH 112/170] Disabled a linter warning --- sonyapilib/device.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 7c2dd21..63188c9 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -518,6 +518,7 @@ def _recreate_auth_cookie(self): The default cookie is for URL/sony. For some commands we need it for the root path """ + # pylint: abstract-class-instantiated cookies = requests.cookies.RequestsCookieJar() cookies.set("auth", self.cookies.get("auth")) return cookies From cd1ee9d97ba455353d3837da01c7804db8499221 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 14 Apr 2019 18:25:46 +0200 Subject: [PATCH 113/170] added reading of mac address for v4 --- sonyapilib/device.py | 18 ++++++++++++++- tests/data/systemInformation.json | 15 ++++++++++++ tests/device_test.py | 38 ++++++++++++++++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 tests/data/systemInformation.json diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 63188c9..0dfea9b 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -153,6 +153,8 @@ def _update_service_urls(self): self._parse_ircc() self._parse_action_list() self._parse_system_information() + else: + self._parse_system_information_v4() except Exception as ex: # pylint: disable=broad-except _LOGGER.error("failed to get device information: %s", str(ex)) @@ -212,6 +214,20 @@ def _parse_ircc(self): service_url = lirc_url.scheme + "://" + lirc_url.netloc self.control_url = service_url + service_location + def _parse_system_information_v4(self): + url = urljoin(self.base_url, "system") + json_data = self._create_api_json("getSystemSupportedFunction") + response = self._send_http(url, HttpMethod.POST, json=json_data) + if not response: + _LOGGER.debug("no response received, device might be off") + return + + json_resp = response.json() + if json_resp and not json_resp.get('error'): + for option in json_resp.get('result')[0]: + if option['option'] == 'WOL': + self.mac = option['value'] + def _parse_system_information(self): response = self._send_http( self._get_action("getSystemInformation").url, method=HttpMethod.GET) @@ -518,7 +534,7 @@ def _recreate_auth_cookie(self): The default cookie is for URL/sony. For some commands we need it for the root path """ - # pylint: abstract-class-instantiated + # pylint: disable=abstract-class-instantiated cookies = requests.cookies.RequestsCookieJar() cookies.set("auth", self.cookies.get("auth")) return cookies diff --git a/tests/data/systemInformation.json b/tests/data/systemInformation.json new file mode 100644 index 0000000..c7485f6 --- /dev/null +++ b/tests/data/systemInformation.json @@ -0,0 +1,15 @@ +{ + "id": 1, + "result": [ + [ + { + "option": "WOL", + "value": "10:08:B1:31:81:B5" + }, + { + "option": "SupportedChineseSoftwareKeyboard", + "value": "no" + } + ] + ] +} \ No newline at end of file diff --git a/tests/device_test.py b/tests/device_test.py index f467f35..fcfd227 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -27,6 +27,7 @@ DMR_URL = 'http://test:52323/dmr.xml' IRCC_URL = 'http://test:50001/Ircc.xml' SYSTEM_INFORMATION_URL = 'http://192.168.240.4:50002/getSystemInformation' +SYSTEM_INFORMATION_URL_V4 = 'http://test/sony/system' GET_REMOTE_COMMAND_LIST_URL = 'http://192.168.240.4:50002/getRemoteCommandList' REGISTRATION_URL_LEGACY = 'http://192.168.240.4:50002/register' REGISTRATION_URL_V4 = 'http://192.168.170.23/sony/accessControl' @@ -44,6 +45,11 @@ AV_TRANSPORT_URL = 'http://test:52323/upnp/control/AVTransport' AV_TRANSPORT_URL_NO_MEDIA = 'http://test2:52323/upnp/control/AVTransport' + +def mock_request_error(*args, **kwargs): + raise HTTPError() + + def mock_error(*args, **kwargs): raise Exception() @@ -121,6 +127,9 @@ def mocked_requests_post(*args, **kwargs): elif url == COMMAND_LIST_V4: json_data = jsonpickle.decode(read_file('data/commandList.json')) return MockResponse(json_data, 200, "") + elif url == SYSTEM_INFORMATION_URL_V4: + json_data = jsonpickle.decode(read_file('data/systemInformation.json')) + return MockResponse(json_data, 200, "") else: raise ValueError("Unknown url requested: {}".format(url)) @@ -219,6 +228,15 @@ def test_update_service_urls_v3(self, mock_ircc, mock_action_list, self.assertEqual(mock_action_list.call_count, 1) self.assertEqual(mock_system_information.call_count, 1) + @mock.patch('requests.get', side_effect=mocked_requests_get) + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_update_service_urls_v4(self, mocked_requests_post, mocked_requests_get): + device = self.create_device() + device.pin = 1234 + device.api_version = 4 + device._update_service_urls() + self.assertEqual(device.mac, "10:08:B1:31:81:B5") + @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_dmr_v3(self, mock_get): content = read_file("data/dmr_v3.xml") @@ -300,7 +318,10 @@ def test_parse_command_list_error(self): for version in versions: device = self.prepare_test_action_list() device.api_version = version - device._parse_command_list() + if version < 4: + device._parse_command_list() + else: + device._parse_command_list_v4() @mock.patch('requests.get', side_effect=mocked_requests_get) @mock.patch('requests.post', side_effect=mocked_requests_post) @@ -625,6 +646,12 @@ def test_get_power_status_false(self): device.api_version = version self.assertFalse(device.get_power_status()) + @mock.patch('requests.post', side_effect=mock_request_error) + def test_get_power_status_error(self, mocked_request_error): + device = self.create_device() + device.api_version = 4 + self.assertFalse(device.get_power_status()) + @mock.patch('requests.get', side_effect=mocked_requests_get) @mock.patch('requests.post', side_effect=mocked_requests_post) def test_get_power_status_true(self, mocked_post, mocked_get): @@ -653,6 +680,15 @@ def test_power_on(self, mock_wake_on_lan, mock_send_command, mock_get_power_stat self.assertEqual(mock_wake_on_lan.call_count, 1) self.assertEqual(mock_send_command.mock_calls[0][1][0], "Power") + @mock.patch('wakeonlan.send_magic_packet', side_effect=mock_nothing()) + def test_wake_on_lan(self, mocked_wol): + device = self.create_device() + device.wakeonlan() + self.assertEqual(mocked_wol.call_count, 0) + device.mac = "foobar" + device.wakeonlan() + self.assertEqual(mocked_wol.call_count, 1) + @mock.patch('requests.post', side_effect=mocked_requests_post) def test_playing_status_no_media_legacy(self, mocked_requests_post): device = self.create_device() From 20e6e8ae97d1dee45f621eb63b0864938cf23aa7 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 14 Apr 2019 18:54:31 +0200 Subject: [PATCH 114/170] added psk support. --- sonyapilib/device.py | 6 +++++- tests/device_test.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 0dfea9b..381ae15 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -72,7 +72,7 @@ class SonyDevice: # pylint: disable=fixme """Contains all data for the device.""" - def __init__(self, host, nickname): + def __init__(self, host, nickname, psk=None): """Init the device with the entry point.""" self.host = host self.nickname = nickname @@ -80,6 +80,7 @@ def __init__(self, host, nickname): self.control_url = None self.av_transport_url = None self.app_url = None + self.psk = psk self.app_port = 50202 self.dmr_port = 52323 @@ -376,6 +377,9 @@ def _recreate_authentication(self): elif registration_action.mode == 4: self.headers['Connection'] = "keep-alive" + if self.psk: + self.headers['X-Auth-PSK'] = self.psk + def _create_api_json(self, method, params=None): # pylint: disable=invalid-name """Create json data which will be send via post for the V4 api""" diff --git a/tests/device_test.py b/tests/device_test.py index fcfd227..6f37d86 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -456,8 +456,12 @@ def test_recreate_authentication_v4(self): self.verify_cookies(device) def test_recreate_authentication_v4_psk(self): - # todo implement psk - pass + device = SonyDevice("test", "test", "foobarPSK") + device.pin = 1234 + self.add_register_to_device(device, 4) + device._recreate_authentication() + self.assertTrue(device.psk) + self.assertEqual(device.headers["X-Auth-PSK"], device.psk) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_register_no_auth(self, mocked_get): From a2eeb2cfa5098b73d517a6ace01e74b5c99e7da2 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Apr 2019 19:43:12 +0200 Subject: [PATCH 115/170] Fixed some issues regarding v3 creation --- sonyapilib/device.py | 26 ++++++++++++++------------ tests/device_test.py | 9 +++++---- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 381ae15..5cb658a 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -111,10 +111,10 @@ def __init__(self, host, nickname, psk=None): def init_device(self): """Update this object with data from the device""" self._update_service_urls() + self._update_commands() if self.pin: self._recreate_authentication() - self._update_commands() self._update_applist() @staticmethod @@ -150,7 +150,7 @@ def _update_service_urls(self): try: self._parse_dmr(response.text) - if self.api_version < 3: + if self.api_version <= 3: self._parse_ircc() self._parse_action_list() self._parse_system_information() @@ -175,12 +175,12 @@ def _parse_action_list(self): action.url, quote(self.nickname), quote(self.get_device_id())) - + self.api_version = action.mode if action.mode == 3: action.url = action.url + "&wolSupport=true" def _parse_ircc(self): - response = self._send_http(self.ircc_url, method=HttpMethod.GET) + response = self._send_http(self.ircc_url, method=HttpMethod.GET, raise_errors=True) if not response: return @@ -297,14 +297,10 @@ def _parse_dmr(self, data): def _update_commands(self): """Update the list of commands.""" - # need to be registered to do that - if not self.pin: - _LOGGER.error("Registration necessary to read command list.") - return - - if self.api_version < 3: + if self.api_version <= 3: self._parse_command_list() - else: + elif self.api_version > 3 and self.pin: + _LOGGER.debug("Registration necessary to read command list.") self._parse_command_list_v4() def _parse_command_list_v4(self): @@ -333,7 +329,13 @@ def _parse_command_list_v4(self): def _parse_command_list(self): """Parse the list of available command in devices with the legacy api.""" - url = self._get_action("getRemoteCommandList").url + action_name = "getRemoteCommandList" + if action_name not in self.actions: + _LOGGER.debug("Action list not set in device, try calling init_device") + return + + action = self.actions[action_name] + url = action.url response = self._send_http(url, method=HttpMethod.GET) if not response: _LOGGER.debug("Failed to get response for command list, device might be off") diff --git a/tests/device_test.py b/tests/device_test.py index 6f37d86..738547b 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -8,7 +8,7 @@ ) import jsonpickle -from requests import HTTPError, URLRequired +from requests import HTTPError, URLRequired, RequestException from tests.testutil import read_file @@ -173,7 +173,7 @@ def test_init_device_no_pin(self, mock_update_applist, mock_update_command, device.init_device() self.assertEqual(mock_update_service_url.call_count, 1) self.assertEqual(mock_recreate_auth.call_count, 0) - self.assertEqual(mock_update_command.call_count, 0) + self.assertEqual(mock_update_command.call_count, 1) self.assertEqual(mock_update_applist.call_count, 0) @mock.patch('sonyapilib.device.SonyDevice._update_service_urls', side_effect=mock_nothing) @@ -260,7 +260,8 @@ def test_parse_dmr_v4(self, mock_get): def test_parse_ircc_error(self): device = self.create_device() - device._parse_ircc() + with self.assertRaises(RequestException): + device._parse_ircc() @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_ircc(self, mock_get): @@ -346,7 +347,7 @@ def test_parse_command_list(self, mock_get, mock_post): def test_update_commands_no_pin(self, mock_parse_cmd_list): device = self.create_device() device._update_commands() - self.assertEqual(mock_parse_cmd_list.call_count, 0) + self.assertEqual(mock_parse_cmd_list.call_count, 1) @mock.patch('sonyapilib.device.SonyDevice._parse_command_list', side_effect=mock_nothing) def test_update_commands_v3(self, mock_parse_cmd_list): From 8fbf9e1f00aab14466e9cbe8a04e6d827b17a63e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Apr 2019 20:46:25 +0200 Subject: [PATCH 116/170] Fixed some issues regarding inital device creation in v3 --- sonyapilib/device.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 5cb658a..aff5f19 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -94,7 +94,6 @@ def __init__(self, host, nickname, psk=None): self.apps = {} self.pin = None - self.name = None self.cookies = None self.mac = None self.api_version = 0 @@ -139,6 +138,8 @@ def load_from_json(data): def save_to_json(self): """Save this device configuration into a json.""" + # make sure object is up to date + self.init_device() return jsonpickle.dumps(self) def _update_service_urls(self): From e221ad0fe7f59600f2bcabb26dd1a5db6b67a5ed Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Apr 2019 21:14:48 +0200 Subject: [PATCH 117/170] Renamed uuid field to client_id --- sonyapilib/device.py | 4 ++-- tests/device_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index aff5f19..6772557 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -97,7 +97,7 @@ def __init__(self, host, nickname, psk=None): self.cookies = None self.mac = None self.api_version = 0 - self.uuid = uuid.uuid4() + self.client_id = uuid.uuid4() ircc_base = "http://{0.host}:{0.ircc_port}".format(self) self.ircc_url = urljoin(ircc_base, "/Ircc.xml") @@ -548,7 +548,7 @@ def _recreate_auth_cookie(self): def get_device_id(self): """Returns the id which is used for the registration.""" - return "TVSideView:{0}".format(self.uuid) + return "TVSideView:{0}".format(self.client_id) def register(self): """ diff --git a/tests/device_test.py b/tests/device_test.py index 738547b..6a389c0 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -202,7 +202,7 @@ def test_save_load_from_json(self): restored_device = SonyDevice.load_from_json(jdata) jdata_restored = restored_device.save_to_json() self.assertEqual(jdata, jdata_restored) - self.assertEqual(restored_device.uuid, device.uuid) + self.assertEqual(restored_device.uuid, device.client_id) def test_update_service_urls_error_response(self): device = self.create_device() From 0065ab757c93c25986119822f4274ee7a3d7afc5 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Apr 2019 21:54:39 +0200 Subject: [PATCH 118/170] Using nickname as id. --- sonyapilib/device.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 6772557..9528c68 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -4,7 +4,6 @@ import base64 import json import logging -import uuid import xml.etree.ElementTree from enum import Enum from urllib.parse import ( @@ -76,6 +75,7 @@ def __init__(self, host, nickname, psk=None): """Init the device with the entry point.""" self.host = host self.nickname = nickname + self.client_id = nickname self.actionlist_url = None self.control_url = None self.av_transport_url = None @@ -97,7 +97,6 @@ def __init__(self, host, nickname, psk=None): self.cookies = None self.mac = None self.api_version = 0 - self.client_id = uuid.uuid4() ircc_base = "http://{0.host}:{0.ircc_port}".format(self) self.ircc_url = urljoin(ircc_base, "/Ircc.xml") @@ -175,7 +174,7 @@ def _parse_action_list(self): action.url = "{0}?name={1}®istrationType=initial&deviceId={2}".format( action.url, quote(self.nickname), - quote(self.get_device_id())) + quote(self.client_id)) self.api_version = action.mode if action.mode == 3: action.url = action.url + "&wolSupport=true" @@ -376,7 +375,7 @@ def _recreate_authentication(self): self.headers['Authorization'] = "Basic %s" % base64string if registration_action.mode == 3: - self.headers['X-CERS-DEVICE-ID'] = self.get_device_id() + self.headers['X-CERS-DEVICE-ID'] = self.client_id elif registration_action.mode == 4: self.headers['Connection'] = "keep-alive" @@ -388,10 +387,10 @@ def _create_api_json(self, method, params=None): """Create json data which will be send via post for the V4 api""" if not params: params = [{ - "clientid": self.get_device_id(), + "clientid": self.client_id, "nickname": self.nickname }, [{ - "clientid": self.get_device_id(), + "clientid": self.client_id, "nickname": self.nickname, "value": "yes", "function": "WOL" @@ -546,10 +545,6 @@ def _recreate_auth_cookie(self): cookies.set("auth", self.cookies.get("auth")) return cookies - def get_device_id(self): - """Returns the id which is used for the registration.""" - return "TVSideView:{0}".format(self.client_id) - def register(self): """ Register at the api. The name which will be displayed in the UI of the device. From a299eab1bcd363e9420dcfd8a3c0780633237da8 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 17 Apr 2019 22:10:49 +0200 Subject: [PATCH 119/170] Fixed tests. --- tests/device_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/device_test.py b/tests/device_test.py index 6a389c0..08491b8 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -202,7 +202,7 @@ def test_save_load_from_json(self): restored_device = SonyDevice.load_from_json(jdata) jdata_restored = restored_device.save_to_json() self.assertEqual(jdata, jdata_restored) - self.assertEqual(restored_device.uuid, device.client_id) + self.assertEqual(restored_device.client_id, device.client_id) def test_update_service_urls_error_response(self): device = self.create_device() @@ -444,7 +444,7 @@ def test_recreate_authentication_v3(self): device._recreate_authentication() self.assertEqual(device.headers["Authorization"], "Basic OjEyMzQ=") - self.assertEqual(device.headers["X-CERS-DEVICE-ID"], device.get_device_id()) + self.assertEqual(device.headers["X-CERS-DEVICE-ID"], device.client_id) def test_recreate_authentication_v4(self): device = self.create_device() From 4360038c9961580110699a70c0469d942fbb2f20 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Sep 2019 19:06:18 +0200 Subject: [PATCH 120/170] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 86a1644..3977745 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Code has been taken from the following repositories. * https://github.com/KHerron/SonyAPILib * https://github.com/aparraga/braviarc -This library is used as communication interface in a home assistant component to control media players, which can be found here: https://github.com/dilruacs/media_player.sony +This library is used as communication interface in a home assistant component to control media players, which can be found here: https://github.com/alexmohr/media_player.sony At the moment not all functions offered by the api are implemented. If you miss a function feel free to create a pull request or open a feature request. From d6d5f3e3410c93afe052f2e67cae5a4858b29c48 Mon Sep 17 00:00:00 2001 From: Matthieu DUVAL Date: Sun, 6 Oct 2019 19:34:14 +0200 Subject: [PATCH 121/170] =?UTF-8?q?Add=20support=20for=20volume=20control?= =?UTF-8?q?=20for=20Home=20Cin=C3=A9ma=20like=20BVD-E2100?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sonyapilib/device.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 9528c68..8264db4 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -659,6 +659,21 @@ def get_apps(self): """Get the apps from the stored dict.""" return list(self.apps.keys()) + def volume_up(self): + # pylint: disable=invalid-name + """Sends the command 'VolumeUp' to the connected device.""" + self._send_command('VolumeUp') + + def volume_down(self): + # pylint: disable=invalid-name + """Sends the command 'VolumeDown' to the connected device.""" + self._send_command('VolumeDown') + + def mute_volume(self): + # pylint: disable=invalid-name + """Sends the command 'Mute' to the connected device.""" + self._send_command('Mute') + def up(self): # pylint: disable=invalid-name """Sends the command 'up' to the connected device.""" From 687da20b6ed3497eb723df8f0e36539f4dae8c12 Mon Sep 17 00:00:00 2001 From: Matthieu DUVAL Date: Mon, 7 Oct 2019 21:27:44 +0200 Subject: [PATCH 122/170] Add new method to test & remove trailing whitespaces --- sonyapilib/device.py | 6 +++--- tests/device_test.py | 13 ++++++------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 8264db4..99f10ea 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -663,17 +663,17 @@ def volume_up(self): # pylint: disable=invalid-name """Sends the command 'VolumeUp' to the connected device.""" self._send_command('VolumeUp') - + def volume_down(self): # pylint: disable=invalid-name """Sends the command 'VolumeDown' to the connected device.""" self._send_command('VolumeDown') - + def mute_volume(self): # pylint: disable=invalid-name """Sends the command 'Mute' to the connected device.""" self._send_command('Mute') - + def up(self): # pylint: disable=invalid-name """Sends the command 'up' to the connected device.""" diff --git a/tests/device_test.py b/tests/device_test.py index 08491b8..2c28778 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -544,14 +544,11 @@ def test_send_authentication_with_auth(self, mock_register, mock_recreate_auth): @mock.patch('sonyapilib.device.SonyDevice._send_command', side_effect=mock_nothing) def test_commands(self, mock_send_command): device = self.create_device() - methods = ["up", "confirm", "down", "right", "left", "home", "options", "returns", "num1", "num2", "num3", - "num4", - "num5", "num6", "num7", "num8", "num9", "num0", "display", "audio", "sub_title", "favorites", - "yellow", - "blue", "red", "green", "play", "stop", "pause", "rewind", "forward", "prev", "next", "replay", - "advance", + methods = ["up", "confirm", "down", "right", "left", "home", "options", "returns", "num1", "num2", "num3", "num4", + "num5", "num6", "num7", "num8", "num9", "num0", "display", "audio", "sub_title", "favorites", "yellow", + "blue", "red", "green", "play", "stop", "pause", "rewind", "forward", "prev", "next", "replay", "advance", "angle", "top_menu", "pop_up_menu", "eject", "karaoke", "netflix", "mode_3d", "zoom_in", "zoom_out", - "browser_back", "browser_forward", "browser_bookmark_list", "list"] + "browser_back", "browser_forward", "browser_bookmark_list", "list", "volume_up", "volume_down" , "mute_volume"] for method in methods: cmd_name = ''.join(x.capitalize() or '_' for x in method.split('_')) # method cannot be named return @@ -559,6 +556,8 @@ def test_commands(self, mock_send_command): cmd_name = "Return" elif method == "mode_3d": cmd_name = "Mode3D" + elif method == "mute_volume": + cmd_name = "Mute" getattr(device, method)() self.assertEqual(mock_send_command.call_count, 1) From fb223d5044205a8f7e83cd47f37543157ccd639a Mon Sep 17 00:00:00 2001 From: Matthieu DUVAL Date: Mon, 7 Oct 2019 22:01:57 +0200 Subject: [PATCH 123/170] Renaming function mute_volume to mute --- sonyapilib/device.py | 2 +- tests/device_test.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 99f10ea..cb07f3d 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -669,7 +669,7 @@ def volume_down(self): """Sends the command 'VolumeDown' to the connected device.""" self._send_command('VolumeDown') - def mute_volume(self): + def mute(self): # pylint: disable=invalid-name """Sends the command 'Mute' to the connected device.""" self._send_command('Mute') diff --git a/tests/device_test.py b/tests/device_test.py index 2c28778..6f98614 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -548,7 +548,7 @@ def test_commands(self, mock_send_command): "num5", "num6", "num7", "num8", "num9", "num0", "display", "audio", "sub_title", "favorites", "yellow", "blue", "red", "green", "play", "stop", "pause", "rewind", "forward", "prev", "next", "replay", "advance", "angle", "top_menu", "pop_up_menu", "eject", "karaoke", "netflix", "mode_3d", "zoom_in", "zoom_out", - "browser_back", "browser_forward", "browser_bookmark_list", "list", "volume_up", "volume_down" , "mute_volume"] + "browser_back", "browser_forward", "browser_bookmark_list", "list", "volume_up", "volume_down" , "mute"] for method in methods: cmd_name = ''.join(x.capitalize() or '_' for x in method.split('_')) # method cannot be named return @@ -556,8 +556,6 @@ def test_commands(self, mock_send_command): cmd_name = "Return" elif method == "mode_3d": cmd_name = "Mode3D" - elif method == "mute_volume": - cmd_name = "Mute" getattr(device, method)() self.assertEqual(mock_send_command.call_count, 1) From 94fc9564952c3cd80c63d924b80683679c6512fa Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Oct 2019 22:13:31 +0200 Subject: [PATCH 124/170] Updated version number for pip --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3df2cb0..97b4db2 100644 --- a/setup.py +++ b/setup.py @@ -19,13 +19,13 @@ setup( name='sonyapilib', packages=['sonyapilib'], # this must be the same as the name above - version='0.4.0', + version='0.4.1', description='Lib to control sony devices with their soap api', author='Alexander Mohr', author_email='sonyapilib@mohr.io', # use the URL to the github repo url='https://github.com/alexmohr/sonyapilib', - download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.0', + download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.1', keywords=['soap', 'sony', 'api'], # arbitrary keywords classifiers=[], install_requires=[ From fc7e22f35db6af2ddbfac0cf9dd221baaada89fb Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 7 Oct 2019 22:49:15 +0200 Subject: [PATCH 125/170] More linting and stages in travis --- .travis.yml | 39 +++++++++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4a4d3b7..938d1c1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,10 +2,41 @@ language: python python: - "3.5" cache: pip +stages: + - UnitTests + - coveralls + - linting install: - pip install -r test_requirements.txt - pip install . -script: - - py.test tests/*_test.py --cov=sonyapilib - - pylint sonyapilib - - coveralls \ No newline at end of file + +jobs: + include: + - stage: UnitTests + name: "Running unit tests" + script: py.test tests/*_test.py --cov=sonyapilib + - stage: coveralls + name: "Running coveralls" + script: coveralls + + # Linting + - stage: linting + install: pip install pycodestyle + name: "Linting with pycodestyle" + script: find . -name \*.py -exec pycodestyle {} + + - stage: linting + install: pip install pydocstyle + name: "Linting with pydocstyle" + script: find . -name \*.py -exec pydocstyle {} + + - stage: linting + install: pip install pylint + name: "Linting with pylint" + script: find . -name \*.py -exec pylint {} + + - stage: linting + install: pip install pyflakes + name: "Linting with pyflakes" + script: find . -name \*.py -exec pyflakes {} + + - stage: linting + install: pip install flake8 + name: "Linting with flake8" + script: find . -name \*.py -exec flake8 {} + From 79b1101c34f21349c2fe9b96008d498f3f02f94a Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Oct 2019 00:04:49 +0200 Subject: [PATCH 126/170] Linting --- .travis.yml | 2 +- examples/convert_to_hass.py | 4 +-- examples/discover_devices.py | 4 +-- sonyapilib/device.py | 65 ++++++++++++++++++++++++------------ sonyapilib/ssdp.py | 4 ++- tests/device_test.py | 39 ++++++++++++++++------ tests/testutil.py | 3 +- 7 files changed, 82 insertions(+), 39 deletions(-) diff --git a/.travis.yml b/.travis.yml index 938d1c1..eba9a7d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ jobs: - stage: linting install: pip install pycodestyle name: "Linting with pycodestyle" - script: find . -name \*.py -exec pycodestyle {} + + script: find . -name \*.py -exec pycodestyle --ignore=E501,E402 {} + - stage: linting install: pip install pydocstyle name: "Linting with pydocstyle" diff --git a/examples/convert_to_hass.py b/examples/convert_to_hass.py index f7ffab6..efb3c81 100644 --- a/examples/convert_to_hass.py +++ b/examples/convert_to_hass.py @@ -5,11 +5,11 @@ config_file = 'bluray.json' with open(config_file, 'r') as myfile: - data=myfile.read() + data = myfile.read() device = SonyDevice.load_from_json(data) hass_cfg = {} hass_cfg[device.host] = {} hass_cfg[device.host]["device"] = data -print(json.dumps(hass_cfg), file=open("sony.conf", "w")) \ No newline at end of file +print(json.dumps(hass_cfg), file=open("sony.conf", "w")) diff --git a/examples/discover_devices.py b/examples/discover_devices.py index cdb33fb..2391d17 100644 --- a/examples/discover_devices.py +++ b/examples/discover_devices.py @@ -4,5 +4,5 @@ ip = "10.0.0.102" ssdp = SSDPDiscovery() services = ssdp.discover() -for service in services: - print(service) +for service in services: + print(service) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index cb07f3d..93a6234 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -170,17 +170,20 @@ def _parse_action_list(self): self.actions[action.name] = action if action.name == "register": - # the authentication later on is based on the device id and the mac - action.url = "{0}?name={1}®istrationType=initial&deviceId={2}".format( - action.url, - quote(self.nickname), - quote(self.client_id)) + # the authentication is based on the device id and the mac + action.url = \ + "{0}?name={1}®istrationType=initial&deviceId={2}"\ + .format( + action.url, + quote(self.nickname), + quote(self.client_id)) self.api_version = action.mode if action.mode == 3: action.url = action.url + "&wolSupport=true" def _parse_ircc(self): - response = self._send_http(self.ircc_url, method=HttpMethod.GET, raise_errors=True) + response = self._send_http( + self.ircc_url, method=HttpMethod.GET, raise_errors=True) if not response: return @@ -231,7 +234,8 @@ def _parse_system_information_v4(self): def _parse_system_information(self): response = self._send_http( - self._get_action("getSystemInformation").url, method=HttpMethod.GET) + self._get_action( + "getSystemInformation").url, method=HttpMethod.GET) if not response: return @@ -328,17 +332,22 @@ def _parse_command_list_v4(self): json.dumps(json_resp, indent=4)) def _parse_command_list(self): - """Parse the list of available command in devices with the legacy api.""" + """ + Parse the list of available command + in devices with the legacy api. + """ action_name = "getRemoteCommandList" if action_name not in self.actions: - _LOGGER.debug("Action list not set in device, try calling init_device") + _LOGGER.debug( + "Action list not set in device, try calling init_device") return action = self.actions[action_name] url = action.url response = self._send_http(url, method=HttpMethod.GET) if not response: - _LOGGER.debug("Failed to get response for command list, device might be off") + _LOGGER.debug( + "Failed to get response for command list, device might be off") return for command in find_in_xml(response.text, [("command", True)]): @@ -353,7 +362,9 @@ def _update_applist(self): else: url = 'http://{}/DIAL/sony/applist'.format(self.host) response = self._send_http( - url, method=HttpMethod.GET, cookies=self._recreate_auth_cookie()) + url, + method=HttpMethod.GET, + cookies=self._recreate_auth_cookie()) if response: for app in find_in_xml(response.text, [(".//app", True)]): @@ -364,7 +375,10 @@ def _update_applist(self): self.apps[data.name] = data def _recreate_authentication(self): - """The default cookie is for URL/sony. For some commands we need it for the root path.""" + """ + The default cookie is for URL/sony. + For some commands we need it for the root path. + """ registration_action = self._get_action("register") if any([not registration_action, registration_action.mode < 3]): return @@ -521,9 +535,11 @@ def _register_v4(self, registration_action): "Content-Type": "application/json" } response = self._send_http(registration_action.url, - method=HttpMethod.POST, headers=headers, + method=HttpMethod.POST, + headers=headers, auth=('', self.pin), - data=json.dumps(authorization), raise_errors=True) + data=json.dumps(authorization), + raise_errors=True) except requests.exceptions.RequestException as ex: return self._handle_register_error(ex) @@ -547,7 +563,8 @@ def _recreate_auth_cookie(self): def register(self): """ - Register at the api. The name which will be displayed in the UI of the device. + Register at the api. + The name which will be displayed in the UI of the device. Make sure this name does not exist yet. For this the device must be put in registration mode. """ @@ -565,7 +582,8 @@ def register(self): registration_result = self._register_v4(registration_action) else: raise ValueError( - "Registration mode {0} is not supported".format(registration_action.mode)) + "Registration mode {0} is not supported" + .format(registration_action.mode)) if registration_result is AuthenticationResult.SUCCESS: self.init_device() @@ -613,14 +631,17 @@ def get_power_status(self): if self.api_version < 4: url = self.actionlist_url try: - self._send_http(url, HttpMethod.GET, log_errors=False, raise_errors=True) + self._send_http(url, HttpMethod.GET, + log_errors=False, raise_errors=True) except requests.exceptions.RequestException as ex: _LOGGER.debug(ex) return False return True try: - resp = self._send_http(urljoin(self.base_url, "system"), HttpMethod.POST, - json=self._create_api_json("getPowerStatus")) + resp = self._send_http(urljoin(self.base_url, "system"), + HttpMethod.POST, + json=self._create_api_json( + "getPowerStatus")) if not resp: return False json_data = resp.json() @@ -641,8 +662,10 @@ def start_app(self, app_name): data = "LOCATION: {0}/run".format(url) self._send_http(url, HttpMethod.POST, data=data) else: - url = 'http://{}/DIAL/apps/{}'.format(self.host, self.apps[app_name].id) - self._send_http(url, HttpMethod.POST, cookies=self._recreate_auth_cookie()) + url = 'http://{}/DIAL/apps/{}'.format( + self.host, self.apps[app_name].id) + self._send_http(url, HttpMethod.POST, + cookies=self._recreate_auth_cookie()) def power(self, power_on, broadcast='255.255.255.255'): """Powers the device on or shuts it off.""" diff --git a/sonyapilib/ssdp.py b/sonyapilib/ssdp.py index 3d5fc29..5490598 100644 --- a/sonyapilib/ssdp.py +++ b/sonyapilib/ssdp.py @@ -32,7 +32,9 @@ def __repr__(self): """ Defines how string representation looks """ - return "".format(**self.__dict__) + return ""\ + .format(**self.__dict__) + class SSDPDiscovery(): # pylint: disable=too-few-public-methods diff --git a/tests/device_test.py b/tests/device_test.py index 6f98614..4d975c2 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -106,30 +106,47 @@ def mocked_requests_post(*args, **kwargs): if not url: raise URLRequired() elif url == REGISTRATION_URL_V4: - return MockResponse({}, 200) + return MockResponse({}, 200) + elif url == REGISTRATION_URL_V4_FAIL: return MockResponse({"error": 402}, 200) + elif url == REGISTRATION_URL_V4_FAIL_401: MockResponse(None, 401).raise_for_status() + elif url == SOAP_URL: return MockResponse({}, 200, "data") + elif url == urljoin(BASE_URL, 'system'): result = MockResponseJson({"status": "on"}) return MockResponse({"result": [result]}, 200) + elif APP_START_URL_LEGACY in url: return MockResponse(None, 200) + elif APP_START_URL in url: return MockResponse(None, 200) + elif url == AV_TRANSPORT_URL: - return MockResponse(None, 200, read_file('data/playing_status_legacy_playing.xml')) + return MockResponse(None, + 200, + read_file( + 'data/playing_status_legacy_playing.xml')) + elif url == AV_TRANSPORT_URL_NO_MEDIA: - return MockResponse(None, 200, read_file('data/playing_status_legacy_no_media.xml')) + return MockResponse(None, + 200, + read_file( + 'data/playing_status_legacy_no_media.xml')) + elif url == COMMAND_LIST_V4: json_data = jsonpickle.decode(read_file('data/commandList.json')) return MockResponse(json_data, 200, "") + elif url == SYSTEM_INFORMATION_URL_V4: json_data = jsonpickle.decode(read_file('data/systemInformation.json')) return MockResponse(json_data, 200, "") + else: raise ValueError("Unknown url requested: {}".format(url)) @@ -149,7 +166,7 @@ def mocked_requests_get(*args, **kwargs): return MockResponse(None, 200, read_file("data/getRemoteCommandList.xml")) elif url == APP_LIST_URL or url == APP_LIST_URL_V4: return MockResponse(None, 200, read_file("data/appsList.xml")) - elif url == REGISTRATION_URL_LEGACY: + elif url == REGISTRATION_URL_LEGACY: return MockResponse({}, 200) elif url == REGISTRATION_URL_V3_FAIL_401: MockResponse(None, 401).raise_for_status() @@ -417,7 +434,7 @@ def test_update_applist(self, mock_get, mock_post, mock_send_command): "PlayStation Video", "Amazon Prime Video", "Netflix", "Rakuten TV", "Tagesschau", "Functions with Gracenote ended", "watchmi Themenkanäle", "Netzkino", "MUBI", "WWE Network", "DW for Smart TV", "YouTube", - "uStudio", "Meteonews TV", "Digital Concert Hall", "Activate Enhanced Features" + "uStudio", "Meteonews TV", "Digital Concert Hall", "Activate Enhanced Features" ] versions = [1, 2, 3, 4] @@ -548,7 +565,7 @@ def test_commands(self, mock_send_command): "num5", "num6", "num7", "num8", "num9", "num0", "display", "audio", "sub_title", "favorites", "yellow", "blue", "red", "green", "play", "stop", "pause", "rewind", "forward", "prev", "next", "replay", "advance", "angle", "top_menu", "pop_up_menu", "eject", "karaoke", "netflix", "mode_3d", "zoom_in", "zoom_out", - "browser_back", "browser_forward", "browser_bookmark_list", "list", "volume_up", "volume_down" , "mute"] + "browser_back", "browser_forward", "browser_bookmark_list", "list", "volume_up", "volume_down", "mute"] for method in methods: cmd_name = ''.join(x.capitalize() or '_' for x in method.split('_')) # method cannot be named return @@ -578,7 +595,7 @@ def register_with_version(self, version, reg_url=""): self.add_register_to_device(device, version) if reg_url: device.actions["register"].url = reg_url - + result = device.register() return [result, device] @@ -618,8 +635,10 @@ def test_get_action(self, mock_init_device): device.actions[action.name] = action self.assertEqual(device._get_action(action.name), action) - @mock.patch('sonyapilib.device.SonyDevice._send_req_ircc', side_effect=mock_nothing) - @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice._send_req_ircc', + side_effect=mock_nothing) + @mock.patch('sonyapilib.device.SonyDevice.init_device', + side_effect=mock_nothing) def test_send_command_error(self, mock_init_device, mock_send_req_ircc): device = self.create_device() with self.assertRaises(ValueError): @@ -701,7 +720,7 @@ def test_playing_status_no_media_legacy(self, mocked_requests_post): device.av_transport_url = AV_TRANSPORT_URL self.assertEqual("PLAYING", device.get_playing_status()) - + @staticmethod def create_command_list(device): command = XmlApiObject({}) diff --git a/tests/testutil.py b/tests/testutil.py index bdc1c7e..60a5029 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -1,7 +1,7 @@ import os.path __location__ = os.path.realpath(os.path.join( - os.getcwd(), os.path.dirname(__file__))) + os.getcwd(), os.path.dirname(__file__))) def read_file(file_name): @@ -15,4 +15,3 @@ def read_file_bin(file_name, size, offset): with open(os.path.join(__location__, file_name), 'rb') as f: f.seek(offset) return f.read(size) - From 4bdbde6dab33760dc68528eb8e6e012bb75e8223 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Oct 2019 10:49:45 +0200 Subject: [PATCH 127/170] linting --- .travis.yml | 7 +- examples/convert_to_hass.py | 1 + examples/discover_devices.py | 2 +- examples/pair_and_apps.py | 3 + examples/readme.py | 2 +- sonyapilib/device.py | 140 +++++++++++++++++------------------ sonyapilib/ssdp.py | 11 +-- sonyapilib/xml_helper.py | 8 +- tests/device_test.py | 35 +++++++-- tests/ssdp_test.py | 6 +- tests/testutil.py | 5 +- 11 files changed, 120 insertions(+), 100 deletions(-) diff --git a/.travis.yml b/.travis.yml index eba9a7d..c9b37ea 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,11 +27,12 @@ jobs: - stage: linting install: pip install pydocstyle name: "Linting with pydocstyle" - script: find . -name \*.py -exec pydocstyle {} + + # do not lint docs for tests + script: find sonyapilib -name \*.py -exec pydocstyle --add-ignore=D400 {} + - stage: linting install: pip install pylint name: "Linting with pylint" - script: find . -name \*.py -exec pylint {} + + script: find sonyapilib -name \*.py -exec pylint {} + - stage: linting install: pip install pyflakes name: "Linting with pyflakes" @@ -39,4 +40,4 @@ jobs: - stage: linting install: pip install flake8 name: "Linting with flake8" - script: find . -name \*.py -exec flake8 {} + + script: find . -name \*.py -exec flake8 --ignore=E501,E402 {} + diff --git a/examples/convert_to_hass.py b/examples/convert_to_hass.py index efb3c81..4c05ecd 100644 --- a/examples/convert_to_hass.py +++ b/examples/convert_to_hass.py @@ -1,3 +1,4 @@ +"""Converts the configuration for home assistant.""" import json from sonyapilib.device import SonyDevice diff --git a/examples/discover_devices.py b/examples/discover_devices.py index 2391d17..e8b898a 100644 --- a/examples/discover_devices.py +++ b/examples/discover_devices.py @@ -1,4 +1,4 @@ -import json +"""Example to discover services on a device.""" from sonyapilib.ssdp import SSDPDiscovery ip = "10.0.0.102" diff --git a/examples/pair_and_apps.py b/examples/pair_and_apps.py index e4e4603..70ae0f2 100644 --- a/examples/pair_and_apps.py +++ b/examples/pair_and_apps.py @@ -1,9 +1,11 @@ +"""Example script to pair the device and start an app.""" from sonyapilib.device import SonyDevice CONFIG_FILE = "bluray.json" def save_device(): + """Save the device to disk.""" data = device.save_to_json() text_file = open(CONFIG_FILE, "w") text_file.write(data) @@ -11,6 +13,7 @@ def save_device(): def load_device(): + """Restore the device from disk.""" import os sony_device = None if os.path.exists(CONFIG_FILE): diff --git a/examples/readme.py b/examples/readme.py index a720f68..bb359a5 100644 --- a/examples/readme.py +++ b/examples/readme.py @@ -1,4 +1,4 @@ -"""Example found in the readme""" +"""Example found in the readme.""" from sonyapilib.device import SonyDevice if __name__ == "__main__": diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 93a6234..502485f 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -1,6 +1,4 @@ -""" -Sony Media player lib -""" +"""Sony Media player lib""" import base64 import json import logging @@ -30,14 +28,16 @@ class AuthenticationResult(Enum): - """Stores the result of the authentication process.""" + """Store the result of the authentication process.""" + SUCCESS = 0 ERROR = 1 PIN_NEEDED = 2 class HttpMethod(Enum): - """Defines which http method is used.""" + """Define which http method is used.""" + GET = "get" POST = "post" @@ -47,6 +47,7 @@ class XmlApiObject: """Holds data for a device action or a command.""" def __init__(self, xml_data): + """Init xml object with given data""" self.name = None self.mode = None self.url = None @@ -300,7 +301,6 @@ def _parse_dmr(self, data): def _update_commands(self): """Update the list of commands.""" - if self.api_version <= 3: self._parse_command_list() elif self.api_version > 3 and self.pin: @@ -332,10 +332,7 @@ def _parse_command_list_v4(self): json.dumps(json_resp, indent=4)) def _parse_command_list(self): - """ - Parse the list of available command - in devices with the legacy api. - """ + """Parse the list of available command in devices with the legacy api.""" action_name = "getRemoteCommandList" if action_name not in self.actions: _LOGGER.debug( @@ -375,10 +372,7 @@ def _update_applist(self): self.apps[data.name] = data def _recreate_authentication(self): - """ - The default cookie is for URL/sony. - For some commands we need it for the root path. - """ + """Recreate auth authentication""" registration_action = self._get_action("register") if any([not registration_action, registration_action.mode < 3]): return @@ -420,7 +414,6 @@ def _create_api_json(self, method, params=None): def _send_http(self, url, method, **kwargs): # pylint: disable=too-many-arguments """Send request command via HTTP json to Sony Bravia.""" - log_errors = kwargs.pop("log_errors", True) raise_errors = kwargs.pop("raise_errors", False) method = kwargs.pop("method", method.value) @@ -467,7 +460,6 @@ def _post_soap_request(self, url, params, action): def _send_req_ircc(self, params): """Send an IRCC command via HTTP to Sony Bravia.""" - data = """ {0} """.format(params) @@ -552,8 +544,9 @@ def _register_v4(self, registration_action): return AuthenticationResult.SUCCESS def _recreate_auth_cookie(self): - """ - The default cookie is for URL/sony. + """Recreate auth cookie for all urls + + Default cookie is for URL/sony. For some commands we need it for the root path """ # pylint: disable=abstract-class-instantiated @@ -562,13 +555,12 @@ def _recreate_auth_cookie(self): return cookies def register(self): - """ - Register at the api. + """Register at the api. + The name which will be displayed in the UI of the device. Make sure this name does not exist yet. For this the device must be put in registration mode. """ - registration_result = AuthenticationResult.ERROR registration_action = registration_action = self._get_action( "register") @@ -608,7 +600,7 @@ def send_authentication(self, pin): return AuthenticationResult.SUCCESS == result def wakeonlan(self, broadcast='255.255.255.255'): - """Starts the device either via wakeonlan.""" + """Start the device either via wakeonlan.""" if self.mac: wakeonlan.send_magic_packet(self.mac, ip_address=broadcast) @@ -627,7 +619,7 @@ def get_playing_status(self): return find_in_xml(content, [".//CurrentTransportState"]).text def get_power_status(self): - """Checks if the device is online.""" + """Check if the device is online.""" if self.api_version < 4: url = self.actionlist_url try: @@ -684,208 +676,208 @@ def get_apps(self): def volume_up(self): # pylint: disable=invalid-name - """Sends the command 'VolumeUp' to the connected device.""" + """Send the command 'VolumeUp' to the connected device.""" self._send_command('VolumeUp') def volume_down(self): # pylint: disable=invalid-name - """Sends the command 'VolumeDown' to the connected device.""" + """Send the command 'VolumeDown' to the connected device.""" self._send_command('VolumeDown') def mute(self): # pylint: disable=invalid-name - """Sends the command 'Mute' to the connected device.""" + """Send the command 'Mute' to the connected device.""" self._send_command('Mute') def up(self): # pylint: disable=invalid-name - """Sends the command 'up' to the connected device.""" + """Send the command 'up' to the connected device.""" self._send_command('Up') def confirm(self): - """Sends the command 'confirm' to the connected device.""" + """Send the command 'confirm' to the connected device.""" self._send_command('Confirm') def down(self): - """Sends the command 'down' to the connected device.""" + """Send the command 'down' to the connected device.""" self._send_command('Down') def right(self): - """Sends the command 'right' to the connected device.""" + """Send the command 'right' to the connected device.""" self._send_command('Right') def left(self): - """Sends the command 'left' to the connected device.""" + """Send the command 'left' to the connected device.""" self._send_command('Left') def home(self): - """Sends the command 'home' to the connected device.""" + """Send the command 'home' to the connected device.""" self._send_command('Home') def options(self): - """Sends the command 'options' to the connected device.""" + """Send the command 'options' to the connected device.""" self._send_command('Options') def returns(self): - """Sends the command 'returns' to the connected device.""" + """Send the command 'returns' to the connected device.""" self._send_command('Return') def num1(self): - """Sends the command 'num1' to the connected device.""" + """Send the command 'num1' to the connected device.""" self._send_command('Num1') def num2(self): - """Sends the command 'num2' to the connected device.""" + """Send the command 'num2' to the connected device.""" self._send_command('Num2') def num3(self): - """Sends the command 'num3' to the connected device.""" + """Send the command 'num3' to the connected device.""" self._send_command('Num3') def num4(self): - """Sends the command 'num4' to the connected device.""" + """Send the command 'num4' to the connected device.""" self._send_command('Num4') def num5(self): - """Sends the command 'num5' to the connected device.""" + """Send the command 'num5' to the connected device.""" self._send_command('Num5') def num6(self): - """Sends the command 'num6' to the connected device.""" + """Send the command 'num6' to the connected device.""" self._send_command('Num6') def num7(self): - """Sends the command 'num7' to the connected device.""" + """Send the command 'num7' to the connected device.""" self._send_command('Num7') def num8(self): - """Sends the command 'num8' to the connected device.""" + """Send the command 'num8' to the connected device.""" self._send_command('Num8') def num9(self): - """Sends the command 'num9' to the connected device.""" + """Send the command 'num9' to the connected device.""" self._send_command('Num9') def num0(self): - """Sends the command 'num0' to the connected device.""" + """Send the command 'num0' to the connected device.""" self._send_command('Num0') def display(self): - """Sends the command 'display' to the connected device.""" + """Send the command 'display' to the connected device.""" self._send_command('Display') def audio(self): - """Sends the command 'audio' to the connected device.""" + """Send the command 'audio' to the connected device.""" self._send_command('Audio') def sub_title(self): - """Sends the command 'subTitle' to the connected device.""" + """Send the command 'subTitle' to the connected device.""" self._send_command('SubTitle') def favorites(self): - """Sends the command 'favorites' to the connected device.""" + """Send the command 'favorites' to the connected device.""" self._send_command('Favorites') def yellow(self): - """Sends the command 'yellow' to the connected device.""" + """Send the command 'yellow' to the connected device.""" self._send_command('Yellow') def blue(self): - """Sends the command 'blue' to the connected device.""" + """Send the command 'blue' to the connected device.""" self._send_command('Blue') def red(self): - """Sends the command 'red' to the connected device.""" + """Send the command 'red' to the connected device.""" self._send_command('Red') def green(self): - """Sends the command 'green' to the connected device.""" + """Send the command 'green' to the connected device.""" self._send_command('Green') def play(self): - """Sends the command 'play' to the connected device.""" + """Send the command 'play' to the connected device.""" self._send_command('Play') def stop(self): - """Sends the command 'stop' to the connected device.""" + """Send the command 'stop' to the connected device.""" self._send_command('Stop') def pause(self): - """Sends the command 'pause' to the connected device.""" + """Send the command 'pause' to the connected device.""" self._send_command('Pause') def rewind(self): - """Sends the command 'rewind' to the connected device.""" + """Send the command 'rewind' to the connected device.""" self._send_command('Rewind') def forward(self): - """Sends the command 'forward' to the connected device.""" + """Send the command 'forward' to the connected device.""" self._send_command('Forward') def prev(self): - """Sends the command 'prev' to the connected device.""" + """Send the command 'prev' to the connected device.""" self._send_command('Prev') def next(self): - """Sends the command 'next' to the connected device.""" + """Send the command 'next' to the connected device.""" self._send_command('Next') def replay(self): - """Sends the command 'replay' to the connected device.""" + """Send the command 'replay' to the connected device.""" self._send_command('Replay') def advance(self): - """Sends the command 'advance' to the connected device.""" + """Send the command 'advance' to the connected device.""" self._send_command('Advance') def angle(self): - """Sends the command 'angle' to the connected device.""" + """Send the command 'angle' to the connected device.""" self._send_command('Angle') def top_menu(self): - """Sends the command 'top_menu' to the connected device.""" + """Send the command 'top_menu' to the connected device.""" self._send_command('TopMenu') def pop_up_menu(self): - """Sends the command 'pop_up_menu' to the connected device.""" + """Send the command 'pop_up_menu' to the connected device.""" self._send_command('PopUpMenu') def eject(self): - """Sends the command 'eject' to the connected device.""" + """Send the command 'eject' to the connected device.""" self._send_command('Eject') def karaoke(self): - """Sends the command 'karaoke' to the connected device.""" + """Send the command 'karaoke' to the connected device.""" self._send_command('Karaoke') def netflix(self): - """Sends the command 'netflix' to the connected device.""" + """Send the command 'netflix' to the connected device.""" self._send_command('Netflix') def mode_3d(self): - """Sends the command 'mode_3d' to the connected device.""" + """Send the command 'mode_3d' to the connected device.""" self._send_command('Mode3D') def zoom_in(self): - """Sends the command 'zoom_in' to the connected device.""" + """Send the command 'zoom_in' to the connected device.""" self._send_command('ZoomIn') def zoom_out(self): - """Sends the command 'zoom_out' to the connected device.""" + """Send the command 'zoom_out' to the connected device.""" self._send_command('ZoomOut') def browser_back(self): - """Sends the command 'browser_back' to the connected device.""" + """Send the command 'browser_back' to the connected device.""" self._send_command('BrowserBack') def browser_forward(self): - """Sends the command 'browser_forward' to the connected device.""" + """Send the command 'browser_forward' to the connected device.""" self._send_command('BrowserForward') def browser_bookmark_list(self): - """Sends the command 'browser_bookmarkList' to the connected device.""" + """Send the command 'browser_bookmarkList' to the connected device.""" self._send_command('BrowserBookmarkList') def list(self): - """Sends the command 'list' to the connected device.""" + """Send the command 'list' to the connected device.""" self._send_command('List') diff --git a/sonyapilib/ssdp.py b/sonyapilib/ssdp.py index 5490598..a963312 100644 --- a/sonyapilib/ssdp.py +++ b/sonyapilib/ssdp.py @@ -1,6 +1,4 @@ -""" -SSDP Implementation -""" +"""SSDP Implementation""" import email import logging import socket @@ -11,9 +9,10 @@ class SSDPResponse: # pylint: disable=too-few-public-methods - """Holds the response of a ssdp request.""" + """Hold the response of a ssdp request.""" def __init__(self, response): + """Init the ssdp response with given data""" if not response: return @@ -29,9 +28,7 @@ def __init__(self, response): self.cache = headers["CACHE-CONTROL"].split("=")[1] def __repr__(self): - """ - Defines how string representation looks - """ + """Define how string representation looks""" return ""\ .format(**self.__dict__) diff --git a/sonyapilib/xml_helper.py b/sonyapilib/xml_helper.py index 9c5ed88..041c835 100644 --- a/sonyapilib/xml_helper.py +++ b/sonyapilib/xml_helper.py @@ -3,7 +3,7 @@ def xml_search_helper(data, param): - """Performs find or findall on given xml with string from param.""" + """Perform find or findall on given xml with string from param.""" if isinstance(param, (tuple, list)) and param[1]: result = data.findall(param[0]) else: @@ -23,8 +23,10 @@ def iterate_search_data(data, param): def find_in_xml(data, search_params): - """Takes an xml from string or as xml.etree.ElementTree and an iterable of - strings (and/or tuples in case of findall) to search. + """Try to find an element in an xml + + Take an xml from string or as xml.etree.ElementTree + and an iterable of strings (and/or tuples in case of findall) to search. The tuple should contain the string to search for and a true value. """ if isinstance(data, str): diff --git a/tests/device_test.py b/tests/device_test.py index 4d975c2..2f67cb9 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -1,3 +1,4 @@ +"""Test implementation for devices""" import os.path import sys import unittest @@ -509,9 +510,18 @@ def test_register_fail_http_timeout(self, mocked_init_device): @mock.patch('requests.get', side_effect=mocked_requests_get) @mock.patch('requests.post', side_effect=mocked_requests_post) @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) - def test_register_fail_pin_needed(self, mocked_init_device, mock_request_get_401, mock_request_post_401): - self.verify_register_fail(3, AuthenticationResult.PIN_NEEDED, mocked_init_device, REGISTRATION_URL_V3_FAIL_401) - self.verify_register_fail(4, AuthenticationResult.PIN_NEEDED, mocked_init_device, REGISTRATION_URL_V4_FAIL_401) + def test_register_fail_pin_needed(self, + mocked_init_device, + mock_request_get_401, + mock_request_post_401): + self.verify_register_fail(3, + AuthenticationResult.PIN_NEEDED, + mocked_init_device, + REGISTRATION_URL_V3_FAIL_401) + self.verify_register_fail(4, + AuthenticationResult.PIN_NEEDED, + mocked_init_device, + REGISTRATION_URL_V4_FAIL_401) @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) @mock.patch('requests.get', side_effect=mocked_requests_get) @@ -561,11 +571,16 @@ def test_send_authentication_with_auth(self, mock_register, mock_recreate_auth): @mock.patch('sonyapilib.device.SonyDevice._send_command', side_effect=mock_nothing) def test_commands(self, mock_send_command): device = self.create_device() - methods = ["up", "confirm", "down", "right", "left", "home", "options", "returns", "num1", "num2", "num3", "num4", - "num5", "num6", "num7", "num8", "num9", "num0", "display", "audio", "sub_title", "favorites", "yellow", - "blue", "red", "green", "play", "stop", "pause", "rewind", "forward", "prev", "next", "replay", "advance", - "angle", "top_menu", "pop_up_menu", "eject", "karaoke", "netflix", "mode_3d", "zoom_in", "zoom_out", - "browser_back", "browser_forward", "browser_bookmark_list", "list", "volume_up", "volume_down", "mute"] + methods = ["up", "confirm", "down", "right", "left", "home", + "options", "returns", "num1", "num2", "num3", "num4", + "num5", "num6", "num7", "num8", "num9", "num0", + "display", "audio", "sub_title", "favorites", "yellow", + "blue", "red", "green", "play", "stop", "pause", + "rewind", "forward", "prev", "next", "replay", "advance", + "angle", "top_menu", "pop_up_menu", "eject", "karaoke", + "netflix", "mode_3d", "zoom_in", "zoom_out", + "browser_back", "browser_forward", "browser_bookmark_list", + "list", "volume_up", "volume_down", "mute"] for method in methods: cmd_name = ''.join(x.capitalize() or '_' for x in method.split('_')) # method cannot be named return @@ -723,21 +738,25 @@ def test_playing_status_no_media_legacy(self, mocked_requests_post): @staticmethod def create_command_list(device): + """Create a list with commands""" command = XmlApiObject({}) command.name = "test" device.commands[command.name] = command @staticmethod def create_device(): + """Create a new device instance""" sonyapilib.device.TIMEOUT = 0.1 device = SonyDevice("test", "test") device.cookies = jsonpickle.decode(read_file("data/cookies.json")) return device def verify_device_dmr(self, device): + """Make sure a dmr has been set""" self.assertEqual(device.av_transport_url, AV_TRANSPORT_URL) def verify_cookies(self, device): + """Make sure a cookie has been set""" self.assertTrue(device.cookies is not None) diff --git a/tests/ssdp_test.py b/tests/ssdp_test.py index a5538d1..38e8b33 100644 --- a/tests/ssdp_test.py +++ b/tests/ssdp_test.py @@ -1,3 +1,4 @@ +"""Test for simple service discovery protocol""" import os.path as path import sys import unittest @@ -18,7 +19,7 @@ def mock_socket(*args, **kwargs): - + """Mock class for request socket""" class MockSocket: def __init__(self): self.offset = 0 @@ -41,8 +42,11 @@ def recv(self, size): class SSDPDiscoveryTest(unittest.TestCase): + """SSDP discovery testing""" + @mock.patch('socket.socket', side_effect=mock_socket) def test_discover(self, mock_socket): + """Test discovery of ssdp services""" discovery = SSDPDiscovery() services = discovery.discover() self.assertEqual(len(services), 9) diff --git a/tests/testutil.py b/tests/testutil.py index 60a5029..92099d4 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -1,3 +1,4 @@ +"""Helper file to find files in tests""" import os.path __location__ = os.path.realpath(os.path.join( @@ -5,13 +6,13 @@ def read_file(file_name): - """ Reads a file from disk """ + """Read a file from disk""" with open(os.path.join(__location__, file_name)) as f: return f.read() def read_file_bin(file_name, size, offset): - """ Reads a file from disk """ + """Read a file from disk""" with open(os.path.join(__location__, file_name), 'rb') as f: f.seek(offset) return f.read(size) From c702b9f19379caa250c5cf0492f45596082e25c7 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Oct 2019 11:03:49 +0200 Subject: [PATCH 128/170] pylint install --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c9b37ea..6cdd7f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,10 @@ jobs: # do not lint docs for tests script: find sonyapilib -name \*.py -exec pydocstyle --add-ignore=D400 {} + - stage: linting - install: pip install pylint + install: + pip install pylint + pip install -r test_requirements.txt + pip install . name: "Linting with pylint" script: find sonyapilib -name \*.py -exec pylint {} + - stage: linting From cb2e83bf374bd3990ccbb56bff0179d33828cfc1 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 8 Oct 2019 11:10:21 +0200 Subject: [PATCH 129/170] travis cfg --- .travis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6cdd7f7..74b0486 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,9 +31,9 @@ jobs: script: find sonyapilib -name \*.py -exec pydocstyle --add-ignore=D400 {} + - stage: linting install: - pip install pylint - pip install -r test_requirements.txt - pip install . + - pip install pylint + - pip install -r test_requirements.txt + - pip install . name: "Linting with pylint" script: find sonyapilib -name \*.py -exec pylint {} + - stage: linting From 5f78348467fb51cbcb3506400d5a1552798558a5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 3 Nov 2019 19:24:16 +0100 Subject: [PATCH 130/170] trigger build. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 74b0486..ee39fe4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -44,3 +44,4 @@ jobs: install: pip install flake8 name: "Linting with flake8" script: find . -name \*.py -exec flake8 --ignore=E501,E402 {} + + \ No newline at end of file From 862e4b9bcc2de21ef11a9d495ae75f18add680a5 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 3 Nov 2019 19:55:16 +0100 Subject: [PATCH 131/170] Travis --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index ee39fe4..f09d056 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ python: cache: pip stages: - UnitTests - - coveralls - linting install: - pip install -r test_requirements.txt @@ -15,9 +14,6 @@ jobs: - stage: UnitTests name: "Running unit tests" script: py.test tests/*_test.py --cov=sonyapilib - - stage: coveralls - name: "Running coveralls" - script: coveralls # Linting - stage: linting From 58e77c7f46dd7807635ea1a70730bdbddba1a826 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 3 Nov 2019 20:06:11 +0100 Subject: [PATCH 132/170] coveralls now working? --- .travis.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f09d056..a0c2e52 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: cache: pip stages: - UnitTests + - coveralls - linting install: - pip install -r test_requirements.txt @@ -14,7 +15,6 @@ jobs: - stage: UnitTests name: "Running unit tests" script: py.test tests/*_test.py --cov=sonyapilib - # Linting - stage: linting install: pip install pycodestyle @@ -40,4 +40,7 @@ jobs: install: pip install flake8 name: "Linting with flake8" script: find . -name \*.py -exec flake8 --ignore=E501,E402 {} + + - stage: coveralls + name: "Running coveralls" + script: coveralls \ No newline at end of file From 503d96a4141eaffbbb9e2c2739c86c8049940e45 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 3 Nov 2019 20:47:07 +0100 Subject: [PATCH 133/170] coveralls --- .travis.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a0c2e52..8a65215 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ python: cache: pip stages: - UnitTests - - coveralls - linting install: - pip install -r test_requirements.txt @@ -14,7 +13,7 @@ jobs: include: - stage: UnitTests name: "Running unit tests" - script: py.test tests/*_test.py --cov=sonyapilib + script: py.test tests/*_test.py --cov=sonyapilib && coveralls # Linting - stage: linting install: pip install pycodestyle @@ -40,7 +39,4 @@ jobs: install: pip install flake8 name: "Linting with flake8" script: find . -name \*.py -exec flake8 --ignore=E501,E402 {} + - - stage: coveralls - name: "Running coveralls" - script: coveralls \ No newline at end of file From 25c4348f5d15e2fd60c3bc9e06666e9a2f96ebc5 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 30 Apr 2020 21:52:28 +0200 Subject: [PATCH 134/170] Added constructor options to give ports --- setup.py | 2 +- sonyapilib/device.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 97b4db2..273fe8f 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='sonyapilib', packages=['sonyapilib'], # this must be the same as the name above - version='0.4.1', + version='0.4.2', description='Lib to control sony devices with their soap api', author='Alexander Mohr', author_email='sonyapilib@mohr.io', diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 502485f..d4c65e0 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -72,7 +72,8 @@ class SonyDevice: # pylint: disable=fixme """Contains all data for the device.""" - def __init__(self, host, nickname, psk=None): + def __init__(self, host, nickname, psk = None, + app_port = 50202, dmr_port = 52323, ircc_port = 50001): """Init the device with the entry point.""" self.host = host self.nickname = nickname @@ -83,9 +84,9 @@ def __init__(self, host, nickname, psk=None): self.app_url = None self.psk = psk - self.app_port = 50202 - self.dmr_port = 52323 - self.ircc_port = 50001 + self.app_port = app_port + self.dmr_port = dmr_port + self.ircc_port = ircc_port # actions are thing like getting status self.actions = {} From fe7849141fd30817483acdb55d85a362bc4f0b97 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 30 Apr 2020 22:13:56 +0200 Subject: [PATCH 135/170] Changed python version in travis.yml --- .travis.yml | 4 ++-- setup.py | 2 +- test_requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8a65215..eeb778e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: python python: - - "3.5" + - "3.7" cache: pip stages: - UnitTests @@ -39,4 +39,4 @@ jobs: install: pip install flake8 name: "Linting with flake8" script: find . -name \*.py -exec flake8 --ignore=E501,E402 {} + - \ No newline at end of file + diff --git a/setup.py b/setup.py index 273fe8f..9f49523 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ author_email='sonyapilib@mohr.io', # use the URL to the github repo url='https://github.com/alexmohr/sonyapilib', - download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.1', + download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.2', keywords=['soap', 'sony', 'api'], # arbitrary keywords classifiers=[], install_requires=[ diff --git a/test_requirements.txt b/test_requirements.txt index 67b727f..a323f61 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -3,4 +3,4 @@ pytest-pep8 pytest-cov python-coveralls pylint -coverage>=4.4 \ No newline at end of file +coverage>=4.4 From 0c66df4cabc412f5c33496ba3828ea6d72c17180 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 30 Apr 2020 22:25:17 +0200 Subject: [PATCH 136/170] Changed coveralls version --- test_requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test_requirements.txt b/test_requirements.txt index a323f61..dd3c718 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -3,4 +3,4 @@ pytest-pep8 pytest-cov python-coveralls pylint -coverage>=4.4 +coverage>=4.5 From ee3151efbefccf4e7c7477bf10894c3387074474 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 30 Apr 2020 22:29:29 +0200 Subject: [PATCH 137/170] Changed to version 4.5.1 for coveralls --- setup.py | 2 +- test_requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 9f49523..8bc53ad 100644 --- a/setup.py +++ b/setup.py @@ -40,6 +40,6 @@ 'pytest-cov', 'python-coveralls', 'pylint', - 'coverage>=4.4' + 'coverage==4.5.4' ] ) diff --git a/test_requirements.txt b/test_requirements.txt index dd3c718..3c787fb 100644 --- a/test_requirements.txt +++ b/test_requirements.txt @@ -1,6 +1,6 @@ -pytest>=3.6 +pytest>=5.4 pytest-pep8 pytest-cov python-coveralls pylint -coverage>=4.5 +coverage==4.5.2 From c2678f24fad5d364dcb1ad98491496b773c1ed2d Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 30 Apr 2020 22:50:46 +0200 Subject: [PATCH 138/170] Fixed deprecation warning --- sonyapilib/device.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index d4c65e0..2b167b7 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -527,10 +527,16 @@ def _register_v4(self, registration_action): headers = { "Content-Type": "application/json" } + + if self.pin is None: + auth_pin = '' + else: + auth_pin = self.pin + response = self._send_http(registration_action.url, method=HttpMethod.POST, headers=headers, - auth=('', self.pin), + auth=('', auth_pin), data=json.dumps(authorization), raise_errors=True) From 3cc98dec068b3e335e83024ccafd2e4c7ccca0b9 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 30 Apr 2020 22:56:26 +0200 Subject: [PATCH 139/170] Fixed linter --- sonyapilib/device.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 2b167b7..ff1af86 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -72,8 +72,9 @@ class SonyDevice: # pylint: disable=fixme """Contains all data for the device.""" - def __init__(self, host, nickname, psk = None, - app_port = 50202, dmr_port = 52323, ircc_port = 50001): + def __init__(self, host, nickname, psk=None, + app_port=50202, dmr_port=52323, ircc_port=50001): + # pylint: disable=too-many-arguments """Init the device with the entry point.""" self.host = host self.nickname = nickname @@ -532,7 +533,7 @@ def _register_v4(self, registration_action): auth_pin = '' else: auth_pin = self.pin - + response = self._send_http(registration_action.url, method=HttpMethod.POST, headers=headers, From 3469e5487af41fb66936b57bd38a91df77093c63 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 30 Apr 2020 23:25:24 +0200 Subject: [PATCH 140/170] Added missing test case --- tests/device_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/device_test.py b/tests/device_test.py index 2f67cb9..d5e5c28 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -489,6 +489,13 @@ def test_register_no_auth(self, mocked_get): result = self.register_with_version(version) self.assertEqual(result[0], AuthenticationResult.SUCCESS) + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_register_auth(self, mocked_get): + versions = [3, 4] + for version in versions: + result = self.register_with_version(version) + self.assertEqual(result[0], AuthenticationResult.SUCCESS) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_register_not_supported(self, mocked_get, mocked_init_device): @@ -607,6 +614,8 @@ def add_register_to_device(device, mode): def register_with_version(self, version, reg_url=""): device = self.create_device() + if version > 2: + device.pin = 1234 self.add_register_to_device(device, version) if reg_url: device.actions["register"].url = reg_url From d15f33e84f04256ba4a81abc9288fe91e5583ff4 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 30 Apr 2020 23:57:28 +0200 Subject: [PATCH 141/170] tests --- sonyapilib/device.py | 2 +- tests/device_test.py | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index ff1af86..4f902c8 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -532,7 +532,7 @@ def _register_v4(self, registration_action): if self.pin is None: auth_pin = '' else: - auth_pin = self.pin + auth_pin = str(self.pin) response = self._send_http(registration_action.url, method=HttpMethod.POST, diff --git a/tests/device_test.py b/tests/device_test.py index d5e5c28..95a5825 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -489,12 +489,6 @@ def test_register_no_auth(self, mocked_get): result = self.register_with_version(version) self.assertEqual(result[0], AuthenticationResult.SUCCESS) - @mock.patch('requests.get', side_effect=mocked_requests_get) - def test_register_auth(self, mocked_get): - versions = [3, 4] - for version in versions: - result = self.register_with_version(version) - self.assertEqual(result[0], AuthenticationResult.SUCCESS) @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) @mock.patch('requests.get', side_effect=mocked_requests_get) From abc49eb9fb9aee1651d6981208b2adc0b0b121ca Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 1 May 2020 00:10:46 +0200 Subject: [PATCH 142/170] tests --- tests/device_test.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/device_test.py b/tests/device_test.py index 95a5825..1750b31 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -497,16 +497,20 @@ def test_register_not_supported(self, mocked_get, mocked_init_device): self.register_with_version(5) self.assertEqual(mocked_init_device.call_count, 0) - def verify_register_fail(self, version, auth_result, mocked_init_device, url=None): - result = self.register_with_version(version, url) + def verify_register_fail(self, version, auth_result, mocked_init_device, url=None, pin=-1): + if pin != -1: + result = self.register_with_version(version, url) + else: + result = self.register_with_version(version, url, pin=pin) self.assertEqual(result[0], auth_result) self.assertEqual(mocked_init_device.call_count, 0) @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) - def test_register_fail_http_timeout(self, mocked_init_device): + def test_register_fail_http_timeout(self, mocked_init_device, pin=-1): versions = [1, 2, 3, 4] for version in versions: - self.verify_register_fail(version, AuthenticationResult.ERROR, mocked_init_device) + if pin != -1: + self.verify_register_fail(version, AuthenticationResult.ERROR, mocked_init_device) @mock.patch('requests.get', side_effect=mocked_requests_get) @mock.patch('requests.post', side_effect=mocked_requests_post) @@ -519,6 +523,11 @@ def test_register_fail_pin_needed(self, AuthenticationResult.PIN_NEEDED, mocked_init_device, REGISTRATION_URL_V3_FAIL_401) + self.verify_register_fail(4, + AuthenticationResult.PIN_NEEDED, + mocked_init_device, + REGISTRATION_URL_V4_FAIL_401, + pin=None) self.verify_register_fail(4, AuthenticationResult.PIN_NEEDED, mocked_init_device, @@ -606,10 +615,10 @@ def add_register_to_device(device, mode): register_action.url = REGISTRATION_URL_V4 device.actions["register"] = register_action - def register_with_version(self, version, reg_url=""): + def register_with_version(self, version, reg_url="", pin=1234): device = self.create_device() if version > 2: - device.pin = 1234 + device.pin = pin self.add_register_to_device(device, version) if reg_url: device.actions["register"].url = reg_url From 1da3a08b99b3d39170fd05056d08f39a52cc8589 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 1 May 2020 00:21:02 +0200 Subject: [PATCH 143/170] added another test case --- tests/device_test.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/device_test.py b/tests/device_test.py index 1750b31..488acff 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -100,6 +100,8 @@ def raise_for_status(self): error.response = self raise error +def mocked_requests_posts_empty(*args, **kwargs): + return {} def mocked_requests_post(*args, **kwargs): url = args[0] @@ -325,6 +327,15 @@ def test_parse_system_information(self, mock_get): device._parse_system_information() self.assertEqual(device.mac, "30-52-cb-cc-16-ee") + @mock.patch('requests.post', side_effect=mocked_requests_posts_empty) + def test_parse_sys_info_error(self, mock_get): + device = self.create_device() + data = XmlApiObject({}) + data.url = SYSTEM_INFORMATION_URL + device.actions["getSystemInformation"] = data + device._parse_system_information() + self.assertEqual(device.mac, None) + def prepare_test_action_list(self): device = self.create_device() data = XmlApiObject({}) @@ -489,7 +500,6 @@ def test_register_no_auth(self, mocked_get): result = self.register_with_version(version) self.assertEqual(result[0], AuthenticationResult.SUCCESS) - @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_register_not_supported(self, mocked_get, mocked_init_device): From f9b87ba2008c0daa4cb9cc66da19c389ac232436 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 1 May 2020 00:33:46 +0200 Subject: [PATCH 144/170] added another exception test case --- tests/device_test.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/device_test.py b/tests/device_test.py index 488acff..0344acb 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -45,6 +45,7 @@ BASE_URL = 'http://test/sony' AV_TRANSPORT_URL = 'http://test:52323/upnp/control/AVTransport' AV_TRANSPORT_URL_NO_MEDIA = 'http://test2:52323/upnp/control/AVTransport' +REQUESTS_ERROR = 'http://ERROR' def mock_request_error(*args, **kwargs): @@ -100,9 +101,11 @@ def raise_for_status(self): error.response = self raise error + def mocked_requests_posts_empty(*args, **kwargs): return {} + def mocked_requests_post(*args, **kwargs): url = args[0] print("POST for URL: {}".format(url)) @@ -150,6 +153,9 @@ def mocked_requests_post(*args, **kwargs): json_data = jsonpickle.decode(read_file('data/systemInformation.json')) return MockResponse(json_data, 200, "") + elif url.startswith(REQUESTS_ERROR): + raise RequestException + else: raise ValueError("Unknown url requested: {}".format(url)) @@ -710,6 +716,13 @@ def test_get_power_status_error(self, mocked_request_error): device.api_version = 4 self.assertFalse(device.get_power_status()) + @mock.patch('requests.post', side_effect=mocked_requests_post) + def test_get_power_status_error2(self, mocked_requests_post): + device = self.create_device() + device.api_version = 4 + device.base_url = REQUESTS_ERROR + self.assertFalse(device.get_power_status()) + @mock.patch('requests.get', side_effect=mocked_requests_get) @mock.patch('requests.post', side_effect=mocked_requests_post) def test_get_power_status_true(self, mocked_post, mocked_get): From bcc4ffe0c5d2c223ebbfd55835d5676e81b2aad8 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 1 May 2020 00:37:52 +0200 Subject: [PATCH 145/170] Fixed missing url --- tests/device_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/device_test.py b/tests/device_test.py index 0344acb..3cebd33 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -721,6 +721,7 @@ def test_get_power_status_error2(self, mocked_requests_post): device = self.create_device() device.api_version = 4 device.base_url = REQUESTS_ERROR + device.actionlist_url = ACTION_LIST_URL self.assertFalse(device.get_power_status()) @mock.patch('requests.get', side_effect=mocked_requests_get) From f56aa80485b4e6776cf528e12e9bd9176a3a8aaa Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 1 May 2020 00:50:39 +0200 Subject: [PATCH 146/170] removed invalid if --- setup.py | 2 +- sonyapilib/device.py | 2 -- tests/device_test.py | 4 ++-- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index 8bc53ad..3095aaa 100644 --- a/setup.py +++ b/setup.py @@ -35,7 +35,7 @@ 'wakeonlan' ], tests_require=[ - 'pytest>=3.6', + 'pytest>=5.4', 'pytest-pep8', 'pytest-cov', 'python-coveralls', diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 4f902c8..031e1b1 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -187,8 +187,6 @@ def _parse_action_list(self): def _parse_ircc(self): response = self._send_http( self.ircc_url, method=HttpMethod.GET, raise_errors=True) - if not response: - return upnp_device = "{}device".format(URN_UPNP_DEVICE) # the action list contains everything the device supports diff --git a/tests/device_test.py b/tests/device_test.py index 3cebd33..3c892df 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -102,7 +102,7 @@ def raise_for_status(self): raise error -def mocked_requests_posts_empty(*args, **kwargs): +def mocked_requests_empty(*args, **kwargs): return {} @@ -333,7 +333,7 @@ def test_parse_system_information(self, mock_get): device._parse_system_information() self.assertEqual(device.mac, "30-52-cb-cc-16-ee") - @mock.patch('requests.post', side_effect=mocked_requests_posts_empty) + @mock.patch('requests.post', side_effect=mocked_requests_empty) def test_parse_sys_info_error(self, mock_get): device = self.create_device() data = XmlApiObject({}) From c3491a0c8ae4743cd0337837425629c81e28a553 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 1 May 2020 01:02:31 +0200 Subject: [PATCH 147/170] tests... --- tests/device_test.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/device_test.py b/tests/device_test.py index 3c892df..69658e1 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -181,6 +181,8 @@ def mocked_requests_get(*args, **kwargs): MockResponse(None, 401).raise_for_status() elif url == GET_REMOTE_CONTROLLER_INFO_URL: return MockResponse(None, 200) + elif url.startswith(REQUESTS_ERROR): + raise RequestException() else: raise ValueError("Unknown url requested: {}".format(url)) @@ -506,6 +508,13 @@ def test_register_no_auth(self, mocked_get): result = self.register_with_version(version) self.assertEqual(result[0], AuthenticationResult.SUCCESS) + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_register_no_auth_error(self, mocked_get): + device = self.create_device() + register_action = XmlApiObject({}) + register_action.url = REQUESTS_ERROR + self.assertEqual(AuthenticationResult.ERROR, device._register_without_auth(register_action)) + @mock.patch('sonyapilib.device.SonyDevice.init_device', side_effect=mock_nothing) @mock.patch('requests.get', side_effect=mocked_requests_get) def test_register_not_supported(self, mocked_get, mocked_init_device): From 4122c1e4313252f531b0e5be2f7ce9dc7fc3103a Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 4 May 2020 19:23:55 +0200 Subject: [PATCH 148/170] Added bugfix for bdp-s580 --- release.sh | 3 +++ sonyapilib/device.py | 9 +++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 release.sh diff --git a/release.sh b/release.sh new file mode 100644 index 0000000..464345c --- /dev/null +++ b/release.sh @@ -0,0 +1,3 @@ +#!/bin/bash +python setup.py sdist +twine upload diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 031e1b1..ed7c458 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -101,8 +101,13 @@ def __init__(self, host, nickname, psk=None, self.mac = None self.api_version = 0 + ircc_base = "http://{0.host}:{0.ircc_port}".format(self) - self.ircc_url = urljoin(ircc_base, "/Ircc.xml") + if self.ircc_port == self.dmr_port: + self.ircc_url = self.dmr_url + else: + self.ircc_url = urljoin(ircc_base, "/Ircc.xml") + self.irccscpd_url = urljoin(ircc_base, "/IRCCSCPD.xml") self.dmr_url = "http://{0.host}:{0.dmr_port}/dmr.xml".format(self) @@ -382,7 +387,7 @@ def _recreate_authentication(self): ('%s:%s' % (username, self.pin)).encode()).decode().replace('\n', '') self.headers['Authorization'] = "Basic %s" % base64string - if registration_action.mode == 3: + if registration_action.mode < 4: self.headers['X-CERS-DEVICE-ID'] = self.client_id elif registration_action.mode == 4: self.headers['Connection'] = "keep-alive" From bdb571099a5255274383a9af11c2926d03256e99 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 4 May 2020 20:09:56 +0200 Subject: [PATCH 149/170] linter --- sonyapilib/device.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index ed7c458..7d0f55f 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -101,13 +101,12 @@ def __init__(self, host, nickname, psk=None, self.mac = None self.api_version = 0 - ircc_base = "http://{0.host}:{0.ircc_port}".format(self) if self.ircc_port == self.dmr_port: self.ircc_url = self.dmr_url else: self.ircc_url = urljoin(ircc_base, "/Ircc.xml") - + self.irccscpd_url = urljoin(ircc_base, "/IRCCSCPD.xml") self.dmr_url = "http://{0.host}:{0.dmr_port}/dmr.xml".format(self) From f24312d302bfb6d3d77dc12c0b10bccd5e9dfa79 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 4 May 2020 20:26:08 +0200 Subject: [PATCH 150/170] linter --- sonyapilib/device.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 7d0f55f..a7333b0 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -101,6 +101,9 @@ def __init__(self, host, nickname, psk=None, self.mac = None self.api_version = 0 + self.dmr_url = "http://{0.host}:{0.dmr_port}/dmr.xml".format(self) + self.app_url = "http://{0.host}:{0.app_port}".format(self) + self.base_url = "http://{0.host}/sony/".format(self) ircc_base = "http://{0.host}:{0.ircc_port}".format(self) if self.ircc_port == self.dmr_port: self.ircc_url = self.dmr_url @@ -109,10 +112,6 @@ def __init__(self, host, nickname, psk=None, self.irccscpd_url = urljoin(ircc_base, "/IRCCSCPD.xml") - self.dmr_url = "http://{0.host}:{0.dmr_port}/dmr.xml".format(self) - self.app_url = "http://{0.host}:{0.app_port}".format(self) - self.base_url = "http://{0.host}/sony/".format(self) - def init_device(self): """Update this object with data from the device""" self._update_service_urls() From 405984ad232e83e59840218147ecfea7e7b94329 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 4 May 2020 23:09:18 +0200 Subject: [PATCH 151/170] Fixed coveralls --- tests/device_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/device_test.py b/tests/device_test.py index 69658e1..4d9155b 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -781,6 +781,10 @@ def test_playing_status_no_media_legacy(self, mocked_requests_post): device.av_transport_url = AV_TRANSPORT_URL self.assertEqual("PLAYING", device.get_playing_status()) + def test_irrc_is_dmr(self): + dev = SonyDevice(host="none", nickname="none", ircc_port=42, dmr_port=42) + self.assertEqual(dev.dmr_url, dev.ircc_url) + @staticmethod def create_command_list(device): """Create a list with commands""" From 512cf077c20b42b65bb083d249ad003568d25381 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Thu, 28 May 2020 15:14:25 +0200 Subject: [PATCH 152/170] version --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3095aaa..d42fe8c 100644 --- a/setup.py +++ b/setup.py @@ -19,13 +19,13 @@ setup( name='sonyapilib', packages=['sonyapilib'], # this must be the same as the name above - version='0.4.2', + version='0.4.3', description='Lib to control sony devices with their soap api', author='Alexander Mohr', author_email='sonyapilib@mohr.io', # use the URL to the github repo url='https://github.com/alexmohr/sonyapilib', - download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.2', + download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.3', keywords=['soap', 'sony', 'api'], # arbitrary keywords classifiers=[], install_requires=[ From c81283c7fa4840005b8f41ff5b50f95a25b47594 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 5 Jun 2020 14:02:57 +0200 Subject: [PATCH 153/170] added device info for issue #53 --- setup.py | 5 ++++- sonyapilib/device.py | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d42fe8c..989317c 100644 --- a/setup.py +++ b/setup.py @@ -19,7 +19,7 @@ setup( name='sonyapilib', packages=['sonyapilib'], # this must be the same as the name above - version='0.4.3', + version='0.4.5', description='Lib to control sony devices with their soap api', author='Alexander Mohr', author_email='sonyapilib@mohr.io', @@ -28,6 +28,9 @@ download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.3', keywords=['soap', 'sony', 'api'], # arbitrary keywords classifiers=[], + setup_requires =[ + 'wheel' + ], install_requires=[ 'jsonpickle', 'setuptools', diff --git a/sonyapilib/device.py b/sonyapilib/device.py index a7333b0..11ccf28 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -387,6 +387,7 @@ def _recreate_authentication(self): self.headers['Authorization'] = "Basic %s" % base64string if registration_action.mode < 4: self.headers['X-CERS-DEVICE-ID'] = self.client_id + self.headers['X-CERS-DEVICE-INFO'] = self.client_id elif registration_action.mode == 4: self.headers['Connection'] = "keep-alive" From 829493163eb44e54460b295c0cd1ade4d64e6a90 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 5 Jun 2020 14:06:38 +0200 Subject: [PATCH 154/170] linter --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 989317c..42db1ea 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.3', keywords=['soap', 'sony', 'api'], # arbitrary keywords classifiers=[], - setup_requires =[ + setup_requires = [ 'wheel' ], install_requires=[ From 493ed864dfc96515e35ea3d6213cd5ff01f9504b Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Fri, 5 Jun 2020 14:10:12 +0200 Subject: [PATCH 155/170] linter --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 42db1ea..fa5a2dd 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.3', keywords=['soap', 'sony', 'api'], # arbitrary keywords classifiers=[], - setup_requires = [ + setup_requires=[ 'wheel' ], install_requires=[ From fe9b1228837bd3d6e1c915eb16a3470251a84972 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 6 Jun 2020 22:25:40 +0200 Subject: [PATCH 156/170] uses sugesstion from #54 to fix service location bug. --- sonyapilib/device.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 11ccf28..803068e 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -219,7 +219,11 @@ def _parse_ircc(self): service_location = service.find( "{0}controlURL".format(URN_UPNP_DEVICE)).text - service_url = lirc_url.scheme + "://" + lirc_url.netloc + + if service_location.startswith('http://'): + service_url = '' + else: + service_url = lirc_url.scheme + "://" + lirc_url.netloc self.control_url = service_url + service_location def _parse_system_information_v4(self): From 03c0d550ec5974b83d1275cc6bf975f5f6140ef9 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 6 Jun 2020 22:26:44 +0200 Subject: [PATCH 157/170] Added auth request to fix #52 --- sonyapilib/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 803068e..e1f1ffe 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -601,7 +601,7 @@ def send_authentication(self, pin): registration_action = self._get_action("register") # they do not need a pin - if registration_action.mode < 3: + if registration_action.mode < 2: return True if not pin: From 07b2b175b611ddaf5e2946a849dea4ca06e07852 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 6 Jun 2020 22:36:18 +0200 Subject: [PATCH 158/170] Added tests. --- publish.sh | 3 +++ sonyapilib/device.py | 2 +- tests/device_test.py | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 publish.sh diff --git a/publish.sh b/publish.sh new file mode 100644 index 0000000..c5edaaf --- /dev/null +++ b/publish.sh @@ -0,0 +1,3 @@ +#!/bin/bash +python setup.py sdist bdist_wheel +twine upload dist/sonyapilib-0.4.4-py3-none-any.whl dist/sonyapilib-0.4.4.tar.gz dist/sonyapilib-0.4.5-py3-none-any.whl dist/sonyapilib-0.4.5.tar.gz diff --git a/sonyapilib/device.py b/sonyapilib/device.py index e1f1ffe..cae7350 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -219,7 +219,7 @@ def _parse_ircc(self): service_location = service.find( "{0}controlURL".format(URN_UPNP_DEVICE)).text - + if service_location.startswith('http://'): service_url = '' else: diff --git a/tests/device_test.py b/tests/device_test.py index 4d9155b..9e8e42a 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -582,7 +582,7 @@ def test_register_success_v4(self, mocked_requests_post, mocked_init_device): @mock.patch('sonyapilib.device.SonyDevice.register', side_effect=mock_nothing) @mock.patch('sonyapilib.device.SonyDevice._recreate_authentication', side_effect=mock_nothing) def test_send_authentication_no_auth(self, mock_register, mock_recreate_auth): - versions = [[1, True], [2, True], [3, False], [4, False]] + versions = [[1, True], [2, False], [3, False], [4, False]] for version in versions: device = self.create_device() self.add_register_to_device(device, version[0]) From 19e4ebd40c88e492228b35bb868445052fbadc08 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 6 Jun 2020 23:33:49 +0200 Subject: [PATCH 159/170] Added test for no schema add --- tests/data/ircc_no_schema.xml | 74 +++++++++++++++++++++++++++++++++++ tests/device_test.py | 13 ++++++ 2 files changed, 87 insertions(+) create mode 100644 tests/data/ircc_no_schema.xml diff --git a/tests/data/ircc_no_schema.xml b/tests/data/ircc_no_schema.xml new file mode 100644 index 0000000..48c9724 --- /dev/null +++ b/tests/data/ircc_no_schema.xml @@ -0,0 +1,74 @@ + + + + 1 + 0 + + + urn:schemas-upnp-org:device:Basic:1 + Blu-ray Disc Player + Sony Corporation + http://www.sony.net/ + + Blu-ray Disc Player + + uuid:00000003-0000-1010-8000-fcf1524e7a1e + + + image/jpeg + 120 + 120 + 24 + /bdp_ax3d_device_icon_large.jpg + + + image/png + 120 + 120 + 24 + /bdp_ax3d_device_icon_large.png + + + image/jpeg + 48 + 48 + 24 + /bdp_ax3d_device_icon_small.jpg + + + image/png + 48 + 48 + 24 + /bdp_ax3d_device_icon_small.png + + + + + urn:schemas-sony-com:service:IRCC:1 + urn:schemas-sony-com:serviceId:IRCC + /IRCCSCPD.xml + http://test:50001/upnp/control/IRCC + + + + + + 1.0 + + + AAMAABxa + + + + + 1.3 + http://192.168.240.4:50002/actionList + + + 1.0 + false + 50004 + + + \ No newline at end of file diff --git a/tests/device_test.py b/tests/device_test.py index 9e8e42a..a6e261a 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -27,6 +27,7 @@ ACTION_LIST_URL = 'http://192.168.240.4:50002/actionList' DMR_URL = 'http://test:52323/dmr.xml' IRCC_URL = 'http://test:50001/Ircc.xml' +IRCC_URL_NO_SCHEMA = 'http://test_no_schema:50001/Ircc.xml' SYSTEM_INFORMATION_URL = 'http://192.168.240.4:50002/getSystemInformation' SYSTEM_INFORMATION_URL_V4 = 'http://test/sony/system' GET_REMOTE_COMMAND_LIST_URL = 'http://192.168.240.4:50002/getRemoteCommandList' @@ -167,6 +168,8 @@ def mocked_requests_get(*args, **kwargs): return MockResponse(None, 200, read_file("data/dmr_v3.xml")) elif url == IRCC_URL: return MockResponse(None, 200, read_file("data/ircc.xml")) + elif url == IRCC_URL_NO_SCHEMA: + return MockResponse(None, 200, read_file("data/ircc_no_schema.xml")) elif url == ACTION_LIST_URL: return MockResponse(None, 200, read_file("data/actionlist.xml")) elif url == SYSTEM_INFORMATION_URL: @@ -300,6 +303,16 @@ def test_parse_ircc(self, mock_get): self.assertEqual( device.control_url, 'http://test:50001/upnp/control/IRCC') + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_parse_ircc_no_schema(self, mock_get): + device = self.create_device() + device.ircc_url = IRCC_URL_NO_SCHEMA + device._parse_ircc() + self.assertEqual( + device.actionlist_url, ACTION_LIST_URL) + self.assertEqual( + device.control_url, 'http://test:50001/upnp/control/IRCC') + def test_parse_action_list_error(self): # just make sure nothing crashes device = self.create_device() From 7b6e7769a5d14cdeb6aef1eec659ce48d54dd392 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 8 Jun 2020 14:39:30 +0200 Subject: [PATCH 160/170] adding device info headers int init device to make sure they are always present. --- sonyapilib/device.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index cae7350..625ce70 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -117,6 +117,9 @@ def init_device(self): self._update_service_urls() self._update_commands() + self.headers['X-CERS-DEVICE-ID'] = self.client_id + self.headers['X-CERS-DEVICE-INFO'] = self.client_id + if self.pin: self._recreate_authentication() self._update_applist() @@ -389,10 +392,7 @@ def _recreate_authentication(self): ('%s:%s' % (username, self.pin)).encode()).decode().replace('\n', '') self.headers['Authorization'] = "Basic %s" % base64string - if registration_action.mode < 4: - self.headers['X-CERS-DEVICE-ID'] = self.client_id - self.headers['X-CERS-DEVICE-INFO'] = self.client_id - elif registration_action.mode == 4: + if registration_action.mode == 4: self.headers['Connection'] = "keep-alive" if self.psk: From da4d5baa7be48182c8bfab0dab38f9a022515017 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 8 Jun 2020 14:43:07 +0200 Subject: [PATCH 161/170] test fixed --- sonyapilib/device.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 625ce70..aed4054 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -116,9 +116,7 @@ def init_device(self): """Update this object with data from the device""" self._update_service_urls() self._update_commands() - - self.headers['X-CERS-DEVICE-ID'] = self.client_id - self.headers['X-CERS-DEVICE-INFO'] = self.client_id + self._add_headers() if self.pin: self._recreate_authentication() @@ -387,6 +385,7 @@ def _recreate_authentication(self): if any([not registration_action, registration_action.mode < 3]): return + self._add_headers() username = '' base64string = base64.encodebytes( ('%s:%s' % (username, self.pin)).encode()).decode().replace('\n', '') @@ -557,6 +556,11 @@ def _register_v4(self, registration_action): self.cookies = response.cookies return AuthenticationResult.SUCCESS + def _add_headers(self): + """Add headers which all devices need""" + self.headers['X-CERS-DEVICE-ID'] = self.client_id + self.headers['X-CERS-DEVICE-INFO'] = self.client_id + def _recreate_auth_cookie(self): """Recreate auth cookie for all urls From bb9baeeea52f6c167d773e4a374e27afd5243a1c Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Mon, 8 Jun 2020 18:50:30 +0200 Subject: [PATCH 162/170] Fixed tests. --- sonyapilib/device.py | 1 + tests/device_test.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index aed4054..ea74635 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -111,6 +111,7 @@ def __init__(self, host, nickname, psk=None, self.ircc_url = urljoin(ircc_base, "/Ircc.xml") self.irccscpd_url = urljoin(ircc_base, "/IRCCSCPD.xml") + self._add_headers() def init_device(self): """Update this object with data from the device""" diff --git a/tests/device_test.py b/tests/device_test.py index a6e261a..3e3469a 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -485,7 +485,9 @@ def test_recreate_authentication_no_auth(self): device = self.create_device() self.add_register_to_device(device, version) device._recreate_authentication() - self.assertEqual(len(device.headers), 0) + self.assertEqual(len(device.headers), 2) + self.assertTrue(device.headers['X-CERS-DEVICE-ID'] == device.nickname) + self.assertTrue(device.headers['X-CERS-DEVICE-INFO'] == device.nickname) def test_recreate_authentication_v3(self): device = self.create_device() From f2a6b8e75ede81282cbfef227225b3ce3f2671f7 Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Sun, 30 Aug 2020 21:34:59 +0200 Subject: [PATCH 163/170] SonyDevice: Support 2010 Blu-ray players, e.g. BDP-S370 --- sonyapilib/device.py | 133 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 126 insertions(+), 7 deletions(-) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index ea74635..611c006 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -2,6 +2,7 @@ import base64 import json import logging +import struct import xml.etree.ElementTree from enum import Enum from urllib.parse import ( @@ -42,6 +43,68 @@ class HttpMethod(Enum): POST = "post" +class IrccCategory(Enum): + TV1 = 1 + AUSYS3 = 80 + TV1EEE = 119 + TV1E = 164 + AUSYS3E = 208 + AUSYS3SE = 528 + AUSYS3EE = 1552 + DVD4 = 3578 + DVD4E = 3834 + BD1 = 7258 + + +IR_KEY_CODES = { + IrccCategory.BD1: ( + ('Num1', 0), + ('Num2', 1), + ('Num3', 2), + ('Num4', 3), + ('Num5', 4), + ('Num6', 5), + ('Num7', 6), + ('Num8', 7), + ('Num9', 8), + ('Num0', 9), + ('Power', 21), + ('Eject', 22), + ('Stop', 24), + ('Pause', 25), + ('Play', 26), + ('Rewind', 27), + ('Forward', 28), + ('PopUpMenu', 41), + ('TopMenu', 44), + ('Up', 57), + ('Down', 58), + ('Left', 59), + ('Right', 60), + ('Confirm', 61), + ('Options', 63), + ('Display', 65), + ('Home', 66), + ('Return', 67), + ('Karaoke', 74), + ('Netflix', 75), + ('Mode3D', 77), + ('Next', 86), + ('Prev', 87), + ('Favorites', 94), + ('SubTitle', 99), + ('Audio', 100), + ('Angle', 101), + ('Blue', 102), + ('Red', 103), + ('Green', 104), + ('Yellow', 105), + ('Advance', 117), + ('Replay', 118), + ) +} + + class XmlApiObject: # pylint: disable=too-few-public-methods """Holds data for a device action or a command.""" @@ -111,6 +174,7 @@ def __init__(self, host, nickname, psk=None, self.ircc_url = urljoin(ircc_base, "/Ircc.xml") self.irccscpd_url = urljoin(ircc_base, "/IRCCSCPD.xml") + self._ircc_categories = set() self._add_headers() def init_device(self): @@ -151,17 +215,22 @@ def save_to_json(self): def _update_service_urls(self): """Initialize the device by reading the necessary resources from it.""" - response = self._send_http(self.dmr_url, method=HttpMethod.GET) - if not response: - _LOGGER.error("Failed to get DMR") + try: + response = self._send_http(self.dmr_url, method=HttpMethod.GET, raise_errors=True) + except requests.exceptions.ConnectionError: + response = None + except requests.exceptions.RequestException as exc: + _LOGGER.error("Failed to get DMR: %s: %s", type(exc), exc) return try: - self._parse_dmr(response.text) + if response: + self._parse_dmr(response.text) if self.api_version <= 3: self._parse_ircc() self._parse_action_list() - self._parse_system_information() + if self.api_version > 0: + self._parse_system_information() else: self._parse_system_information_v4() @@ -177,12 +246,21 @@ def _parse_action_list(self): action = XmlApiObject(element.attrib) self.actions[action.name] = action + if action.mode is None: + action.mode = self.api_version + if action.url is None and action.name: + action.url = urljoin(self.actionlist_url, "?action={}".format(action.name)) + separator = "&" + else: + separator = "?" + if action.name == "register": # the authentication is based on the device id and the mac action.url = \ - "{0}?name={1}®istrationType=initial&deviceId={2}"\ + "{0}{1}name={2}®istrationType=initial&deviceId={3}"\ .format( action.url, + separator, quote(self.nickname), quote(self.client_id)) self.api_version = action.mode @@ -228,6 +306,22 @@ def _parse_ircc(self): service_url = lirc_url.scheme + "://" + lirc_url.netloc self.control_url = service_url + service_location + categories = find_in_xml( + response.text, + [upnp_device, + "{}X_IRCC_DeviceInfo".format(URN_SONY_AV), + "{}X_IRCC_CategoryList".format(URN_SONY_AV), + ("{}X_IRCC_Category".format(URN_SONY_AV), True)] + ) + + for category in categories: + category_info = category.find( + "{}X_CategoryInfo".format(URN_SONY_AV)) + if category_info is None: + continue + + self._ircc_categories.add(category_info.text) + def _parse_system_information_v4(self): url = urljoin(self.base_url, "system") json_data = self._create_api_json("getSystemSupportedFunction") @@ -310,7 +404,9 @@ def _parse_dmr(self, data): def _update_commands(self): """Update the list of commands.""" - if self.api_version <= 3: + if self.api_version == 0: + self._use_builtin_command_list() + elif self.api_version <= 3: self._parse_command_list() elif self.api_version > 3 and self.pin: _LOGGER.debug("Registration necessary to read command list.") @@ -360,6 +456,29 @@ def _parse_command_list(self): name = command.get("name") self.commands[name] = XmlApiObject(command.attrib) + def _use_builtin_command_list(self): + for encoded_str in self._ircc_categories: + fmt, category_id = struct.unpack(">HI", base64.b64decode(encoded_str)) + try: + category = IrccCategory(category_id) + except ValueError: + _LOGGER.warning("Unknown IRCC category identifier: %d", category_id) + continue + + code_list = IR_KEY_CODES.get(category) + if code_list is None: + _LOGGER.warning("No command list available for %s", category) + continue + + for name, code in code_list: + value = base64.b64encode(struct.pack(">IIIB", fmt, category_id, code, 3)) + data = XmlApiObject({ + "name": name, + "type": "ircc", + "value": value.decode("ascii"), + }) + self.commands[name] = data + def _update_applist(self): """Update the list of apps which are supported by the device.""" if self.api_version < 4: From 3e1380bc3e9660766438596ca586789b1723e466 Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Tue, 1 Sep 2020 13:35:44 +0200 Subject: [PATCH 164/170] device_test.py: Default to API version 3 Previously, the lowest supported api_version was 3. Default to it in order to fix existing tests. --- tests/device_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/device_test.py b/tests/device_test.py index 3e3469a..ce41d41 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -812,6 +812,7 @@ def create_device(): """Create a new device instance""" sonyapilib.device.TIMEOUT = 0.1 device = SonyDevice("test", "test") + device.api_version = 3 device.cookies = jsonpickle.decode(read_file("data/cookies.json")) return device From b8059cec453a51b86009cd5a7e139cddda559924 Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Tue, 1 Sep 2020 13:38:54 +0200 Subject: [PATCH 165/170] IrccCategory: Add a short description --- sonyapilib/device.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 611c006..7d871b5 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -44,6 +44,8 @@ class HttpMethod(Enum): class IrccCategory(Enum): + """Device categories used by IRCC.""" + TV1 = 1 AUSYS3 = 80 TV1EEE = 119 From bae0494cca32a28d88342ebe156e5e950264e1ac Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Tue, 1 Sep 2020 13:52:24 +0200 Subject: [PATCH 166/170] travis: disable pylint message C0302 > sonyapilib/device.py:1:0: C0302: Too many lines in module (1023/1000) (too-many-lines) --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index eeb778e..2489647 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,7 +30,7 @@ jobs: - pip install -r test_requirements.txt - pip install . name: "Linting with pylint" - script: find sonyapilib -name \*.py -exec pylint {} + + script: find sonyapilib -name \*.py -exec pylint -d C0302 {} + - stage: linting install: pip install pyflakes name: "Linting with pyflakes" From b9fc63ca2939d83f9fdf6d6b6bbcaf5f27c5235c Mon Sep 17 00:00:00 2001 From: Andreas Oberritter Date: Tue, 1 Sep 2020 14:25:00 +0200 Subject: [PATCH 167/170] Add a test for SonyDevice._use_builtin_command_list --- tests/device_test.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/device_test.py b/tests/device_test.py index ce41d41..79d4875 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -399,6 +399,13 @@ def test_update_commands_no_pin(self, mock_parse_cmd_list): device._update_commands() self.assertEqual(mock_parse_cmd_list.call_count, 1) + @mock.patch('sonyapilib.device.SonyDevice._use_builtin_command_list', side_effect=mock_nothing) + def test_update_commands_v0(self, mock_parse_cmd_list): + device = self.create_device() + device.api_version = 0 + device._update_commands() + self.assertEqual(mock_parse_cmd_list.call_count, 1) + @mock.patch('sonyapilib.device.SonyDevice._parse_command_list', side_effect=mock_nothing) def test_update_commands_v3(self, mock_parse_cmd_list): device = self.create_device() From c9a2445174a2ae972e2aff34e16039aca95f17b0 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 3 Oct 2020 13:14:14 +0200 Subject: [PATCH 168/170] Added tests to fix coverage --- publish.sh | 2 +- sonyapilib/device.py | 6 +- tests/data/actionlist_no_url.xml | 13 ++++ tests/data/ircc_missing_info.xml | 92 ++++++++++++++++++++++++++++ tests/device_test.py | 102 +++++++++++++++++++++++++++---- 5 files changed, 199 insertions(+), 16 deletions(-) mode change 100644 => 100755 publish.sh create mode 100644 tests/data/actionlist_no_url.xml create mode 100644 tests/data/ircc_missing_info.xml diff --git a/publish.sh b/publish.sh old mode 100644 new mode 100755 index c5edaaf..5894050 --- a/publish.sh +++ b/publish.sh @@ -1,3 +1,3 @@ #!/bin/bash python setup.py sdist bdist_wheel -twine upload dist/sonyapilib-0.4.4-py3-none-any.whl dist/sonyapilib-0.4.4.tar.gz dist/sonyapilib-0.4.5-py3-none-any.whl dist/sonyapilib-0.4.5.tar.gz +twine upload dist diff --git a/sonyapilib/device.py b/sonyapilib/device.py index 7d871b5..d65ae6b 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -293,10 +293,8 @@ def _parse_ircc(self): service_id = service.find( "{0}serviceId".format(URN_UPNP_DEVICE)) - if any([ - service_id is None, - URN_SONY_IRCC not in service_id.text, - ]): + if service_id is None or \ + URN_SONY_IRCC not in service_id.text: continue service_location = service.find( diff --git a/tests/data/actionlist_no_url.xml b/tests/data/actionlist_no_url.xml new file mode 100644 index 0000000..204ee08 --- /dev/null +++ b/tests/data/actionlist_no_url.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/data/ircc_missing_info.xml b/tests/data/ircc_missing_info.xml new file mode 100644 index 0000000..7774706 --- /dev/null +++ b/tests/data/ircc_missing_info.xml @@ -0,0 +1,92 @@ + + + + 1 + 0 + + + urn:schemas-upnp-org:device:Basic:1 + Blu-ray Disc Player + Sony Corporation + http://www.sony.net/ + + Blu-ray Disc Player + + uuid:00000003-0000-1010-8000-fcf1524e7a1e + + + image/jpeg + 120 + 120 + 24 + /bdp_ax3d_device_icon_large.jpg + + + image/png + 120 + 120 + 24 + /bdp_ax3d_device_icon_large.png + + + image/jpeg + 48 + 48 + 24 + /bdp_ax3d_device_icon_small.jpg + + + image/png + 48 + 48 + 24 + /bdp_ax3d_device_icon_small.png + + + + + urn:schemas-sony-com:service:IRCC:1 + + + /IRCCSCPD.xml + http://test:50001/upnp/control/IRCC + + + + urn:schemas-sony-com:service:IRCC:1 + + INVALID_DATA + /IRCCSCPD.xml + http://test:50001/upnp/control/IRCC + + + + urn:schemas-sony-com:service:IRCC:1 + urn:schemas-sony-com:serviceId:IRCC + /IRCCSCPD.xml + http://test:50001/upnp/control/IRCC + + + + + + 1.0 + + + + + AAMAABxa + + + + + 1.3 + http://192.168.240.4:50002/actionList + + + 1.0 + false + 50004 + + + \ No newline at end of file diff --git a/tests/device_test.py b/tests/device_test.py index 79d4875..cbcf954 100644 --- a/tests/device_test.py +++ b/tests/device_test.py @@ -25,9 +25,11 @@ ACTION_LIST_URL = 'http://192.168.240.4:50002/actionList' +ACTION_LIST_URL_2 = 'http://192.168.240.4:50002/actionList2' DMR_URL = 'http://test:52323/dmr.xml' IRCC_URL = 'http://test:50001/Ircc.xml' IRCC_URL_NO_SCHEMA = 'http://test_no_schema:50001/Ircc.xml' +IRCC_URL_MISSING_INFO = 'http://test_missing_info:50001/Ircc.xml' SYSTEM_INFORMATION_URL = 'http://192.168.240.4:50002/getSystemInformation' SYSTEM_INFORMATION_URL_V4 = 'http://test/sony/system' GET_REMOTE_COMMAND_LIST_URL = 'http://192.168.240.4:50002/getRemoteCommandList' @@ -48,6 +50,22 @@ AV_TRANSPORT_URL_NO_MEDIA = 'http://test2:52323/upnp/control/AVTransport' REQUESTS_ERROR = 'http://ERROR' +ACTION_LIST = [ + "getText", + "sendText", + "getContentInformation", + "getSystemInformation", + "getRemoteCommandList", + "getStatus", + "getHistoryList", + "getContentUrl", + "sendContentUrl" +] + + +def mocked_return_none(*args, **kwargs): + return None + def mock_request_error(*args, **kwargs): raise HTTPError() @@ -57,6 +75,10 @@ def mock_error(*args, **kwargs): raise Exception() +def mock_request_exception(*args, **kwargs): + raise RequestException("Test Exception") + + def mock_nothing(*args, **kwargs): pass @@ -168,10 +190,14 @@ def mocked_requests_get(*args, **kwargs): return MockResponse(None, 200, read_file("data/dmr_v3.xml")) elif url == IRCC_URL: return MockResponse(None, 200, read_file("data/ircc.xml")) + elif url == IRCC_URL_MISSING_INFO: + return MockResponse(None, 200, read_file("data/ircc_missing_info.xml")) elif url == IRCC_URL_NO_SCHEMA: return MockResponse(None, 200, read_file("data/ircc_no_schema.xml")) elif url == ACTION_LIST_URL: return MockResponse(None, 200, read_file("data/actionlist.xml")) + elif url == ACTION_LIST_URL_2: + return MockResponse(None, 200, read_file("data/actionlist_no_url.xml")) elif url == SYSTEM_INFORMATION_URL: return MockResponse(None, 200, read_file("data/getSysteminformation.xml")) elif url == GET_REMOTE_COMMAND_LIST_URL: @@ -246,6 +272,12 @@ def test_update_service_urls_error_processing(self, mock_error, mocked_requests_ device._update_service_urls() self.assertEqual(mock_error.call_count, 1) + @mock.patch('sonyapilib.device.SonyDevice._send_http', side_effect=mock_request_exception) + def test_update_service_urls_request_exception(self, mock_request_exception): + device = self.create_device() + device._update_service_urls() + self.assertEqual(mock_request_exception.call_count, 1) + @mock.patch('requests.get', side_effect=mocked_requests_get) @mock.patch('sonyapilib.device.SonyDevice._parse_ircc', side_effect=mock_nothing) @mock.patch('sonyapilib.device.SonyDevice._parse_action_list', side_effect=mock_nothing) @@ -313,6 +345,16 @@ def test_parse_ircc_no_schema(self, mock_get): self.assertEqual( device.control_url, 'http://test:50001/upnp/control/IRCC') + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_parse_ircc_no_missing_info(self, mock_get): + device = self.create_device() + device.ircc_url = IRCC_URL_MISSING_INFO + device._parse_ircc() + self.assertEqual( + device.actionlist_url, ACTION_LIST_URL) + self.assertEqual( + device.control_url, 'http://test:50001/upnp/control/IRCC') + def test_parse_action_list_error(self): # just make sure nothing crashes device = self.create_device() @@ -322,23 +364,27 @@ def test_parse_action_list_error(self): @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_action_list(self, mock_get): device = self.create_device() - # must be set before prior methods are not called. + # must be set before methods are not called. device.actionlist_url = ACTION_LIST_URL device._parse_action_list() self.assertEqual(device.actions["register"].mode, 3) - actions = ["getText", - "sendText", - "getContentInformation", - "getSystemInformation", - "getRemoteCommandList", - "getStatus", - "getHistoryList", - "getContentUrl", - "sendContentUrl"] + base_url = "http://192.168.240.4:50002/" - for action in actions: + for action in ACTION_LIST: self.assertEqual(device.actions[action].url, base_url + action) + @mock.patch('requests.get', side_effect=mocked_requests_get) + def test_parse_action_list_without_url(self, mock_get): + device = self.create_device() + # must be set before methods are not called. + device.actionlist_url = ACTION_LIST_URL_2 + device._parse_action_list() + self.assertEqual(device.actions["register"].mode, 3) + + for action in ACTION_LIST: + action_url = "{}?action={}".format(ACTION_LIST_URL_2, action) + self.assertEqual(device.actions[action].url, action_url) + @mock.patch('requests.get', side_effect=mocked_requests_get) def test_parse_system_information(self, mock_get): device = self.create_device() @@ -807,6 +853,40 @@ def test_irrc_is_dmr(self): dev = SonyDevice(host="none", nickname="none", ircc_port=42, dmr_port=42) self.assertEqual(dev.dmr_url, dev.ircc_url) + def test_parse_use_built_in_command_list_invalid_category(self): + device = self.create_device() + device._ircc_categories = ["MTIzNDU2"] + + device._use_builtin_command_list() + self.assertEqual(0, len(device.commands)) + + def test_parse_use_built_in_command_list(self): + device = self.create_device() + device._ircc_categories = ["AAMAABxa"] + + device._use_builtin_command_list() + commands = ["Confirm", "Up", "Down", "Right", "Left", "Home", "Options", + "Return", "Num1", "Num2", "Num3", "Num4", "Num5", "Num6", "Num7", + "Num8", "Num9", "Num0", "Power", "Display", "Audio", "SubTitle", + "Favorites", "Yellow", "Blue", "Red", "Green", "Play", "Stop", + "Pause", "Rewind", "Forward", "Prev", "Next", "Replay", "Advance", + "Angle", "TopMenu", "PopUpMenu", "Eject", "Karaoke", "Netflix", + "Mode3D"] + + for cmd in commands: + self.assertTrue(cmd in device.commands) + + def test_handle_register_error_not_http(self): + ex = Exception() + device = self.create_device() + res = device._handle_register_error(ex) + self.assertEqual(res, AuthenticationResult.ERROR) + + @mock.patch('sonyapilib.device.SonyDevice._send_http', side_effect=mocked_return_none) + def test_parse_system_info_v4_no_response(self, mocked_request): + device = self.create_device() + device._parse_system_information_v4() + @staticmethod def create_command_list(device): """Create a list with commands""" From 50fd5839e5ffe057c472ae41d3c40e98b92b55a0 Mon Sep 17 00:00:00 2001 From: Alexander Mohr Date: Sat, 3 Oct 2020 14:10:54 +0200 Subject: [PATCH 169/170] Version bump --- publish.sh | 1 + setup.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/publish.sh b/publish.sh index 5894050..08ff843 100755 --- a/publish.sh +++ b/publish.sh @@ -1,3 +1,4 @@ #!/bin/bash python setup.py sdist bdist_wheel twine upload dist + diff --git a/setup.py b/setup.py index fa5a2dd..d2bfc62 100644 --- a/setup.py +++ b/setup.py @@ -19,13 +19,13 @@ setup( name='sonyapilib', packages=['sonyapilib'], # this must be the same as the name above - version='0.4.5', + version='0.5.0', description='Lib to control sony devices with their soap api', author='Alexander Mohr', author_email='sonyapilib@mohr.io', # use the URL to the github repo url='https://github.com/alexmohr/sonyapilib', - download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.4.3', + download_url='https://codeload.github.com/alexmohr/sonyapilib/tar.gz/0.5.0', keywords=['soap', 'sony', 'api'], # arbitrary keywords classifiers=[], setup_requires=[ From 8608c5775bd8378f39dff7fe6a50523c1272eb54 Mon Sep 17 00:00:00 2001 From: gohlas <50915149+gohlas@users.noreply.github.com> Date: Mon, 12 Oct 2020 17:47:58 +0200 Subject: [PATCH 170/170] Update device.py Added HDMI support for some models --- sonyapilib/device.py | 59 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/sonyapilib/device.py b/sonyapilib/device.py index d65ae6b..a7f1302 100644 --- a/sonyapilib/device.py +++ b/sonyapilib/device.py @@ -4,6 +4,7 @@ import logging import struct import xml.etree.ElementTree +import time from enum import Enum from urllib.parse import ( urljoin, @@ -1019,3 +1020,61 @@ def browser_bookmark_list(self): def list(self): """Send the command 'list' to the connected device.""" self._send_command('List') + + def input_hdmi1(self): + """Send HDMI input selection to the connected device""" + + self.home() + + time.sleep(1) + + data = """ + 0 + local://{0}::60151/I_14_02_0_-1_00_06_6_23_0_0 + [truncated]<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dlna="urn:schemas-dln + """.format(self.host) + + action = "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI" + + content = self._post_soap_request( + url=self.av_transport_url, params=data, action=action) + + self.input_play() + + return "HDMI 1" + + def input_hdmi2(self): + """Send HDMI input selection to the connected device""" + + self.home() + + time.sleep(1) + + data = """ + 0 + local://{0}:60151/I_14_02_0_-1_00_07_7_23_0_0 + [truncated]<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dlna="urn:schemas-dln + """.format(self.host) + + action = "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI" + + content = self._post_soap_request( + url=self.av_transport_url, params=data, action=action) + + self.input_play() + + return "HDMI 2" + + print(input_hdmi2) + + def input_play(self): + """Send input select to the connected device""" + data = """ + 0 + 1 + """ + + action = "urn:schemas-upnp-org:service:AVTransport:1#Play" + + content = self._post_soap_request( + url=self.av_transport_url, params=data, action=action)