From cfad4e90a293411cb0366569fe0d6285a14d0559 Mon Sep 17 00:00:00 2001 From: Teemu Rytilahti Date: Wed, 7 Oct 2020 00:48:39 +0200 Subject: [PATCH] Allow forcing getSupportedApiInfo response from local devinfo file Some devices do not support the ability to obtain information about available APIs. This PR adds new --devinfo-file option that allows using an existing devinfo file to feed the list of available services, which may be helpful for such devices as long as they implement getMethodTypes() call for the services. Potential fix for #29 --- songpal/device.py | 71 ++++++++++++++++++++++++++++++++-------------- songpal/main.py | 7 +++-- songpal/service.py | 2 +- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/songpal/device.py b/songpal/device.py index 24d5d88..ec443ae 100644 --- a/songpal/device.py +++ b/songpal/device.py @@ -1,6 +1,7 @@ """Module presenting a single supported device.""" import asyncio import itertools +import json import logging from collections import defaultdict from pprint import pformat as pf @@ -43,7 +44,7 @@ class Device: WEBSOCKET_PROTOCOL = "v10.webapi.scalar.sony.com" WEBSOCKET_VERSION = 13 - def __init__(self, endpoint, force_protocol=None, debug=0): + def __init__(self, endpoint, force_protocol=None, debug=0, devinfo_file=None): """Initialize Device. :param endpoint: the main API endpoint. @@ -67,6 +68,28 @@ def __init__(self, endpoint, force_protocol=None, debug=0): self.callbacks = defaultdict(set) + self.devinfo = None + if devinfo_file is not None: + _LOGGER.debug("Using device info file: %s", devinfo_file) + self.devinfo = self._load_devinfo_file(devinfo_file) + + def _load_devinfo_file(self, file): + """Internal method to create getSupportedApiInfo like response. + + This reads an existing devinfo file and creates a minimal data structure + that is enough to construct services objects as long as the service endpoints + expose the getMethodTypes() method. + """ + data = json.load(file) + methods = data["supported_methods"] + devinfo = [] + + for service, values in methods.items(): + serv = {"service": service, "protocols": values["protocols"]} + devinfo.append(serv) + + return devinfo + async def __aenter__(self): """Asynchronous context manager, initializes the list of available methods.""" await self.get_supported_methods() @@ -130,31 +153,35 @@ async def get_supported_methods(self): Calling this as the first thing before doing anything else is necessary to fill the available services table. """ - response = await self.request_supported_methods() + if self.devinfo is None: + response = await self.request_supported_methods() - if "result" in response: - services = response["result"][0] - _LOGGER.debug("Got %s services!" % len(services)) + if "result" in response: + services = response["result"][0] + else: + raise SongpalException("Supported methods responded without result") + else: + services = self.devinfo - for x in services: - serv = await Service.from_payload( - x, self.endpoint, self.idgen, self.debug, self.force_protocol - ) - if serv is not None: - self.services[x["service"]] = serv - else: - _LOGGER.warning("Unable to create service %s", x["service"]) + _LOGGER.debug("Got %s services!" % len(services)) + + for x in services: + serv = await Service.from_payload( + x, self.endpoint, self.idgen, self.debug, self.force_protocol + ) + if serv is not None: + self.services[x["service"]] = serv + else: + _LOGGER.warning("Unable to create service %s", x["service"]) - for service in self.services.values(): + for service in self.services.values(): + if self.debug > 1: + _LOGGER.debug("Service %s", service) + for api in service.methods: + # self.logger.debug("%s > %s" % (service, api)) if self.debug > 1: - _LOGGER.debug("Service %s", service) - for api in service.methods: - # self.logger.debug("%s > %s" % (service, api)) - if self.debug > 1: - _LOGGER.debug("> %s" % api) - return self.services - - return None + _LOGGER.debug("> %s" % api) + return self.services async def get_power(self) -> Power: """Get the device state.""" diff --git a/songpal/main.py b/songpal/main.py index fc80d75..fdf799d 100644 --- a/songpal/main.py +++ b/songpal/main.py @@ -110,10 +110,11 @@ def print_settings(settings, depth=0): @click.option("-d", "--debug", default=False, count=True) @click.option("--post", is_flag=True, required=False) @click.option("--websocket", is_flag=True, required=False) +@click.option("--devinfo-file", type=click.File("r"), required=False) @click.pass_context @click.version_option() @coro -async def cli(ctx, endpoint, debug, websocket, post): +async def cli(ctx, endpoint, debug, websocket, post, devinfo_file): """Songpal CLI.""" lvl = logging.INFO if debug: @@ -139,7 +140,9 @@ async def cli(ctx, endpoint, debug, websocket, post): protocol = ProtocolType.XHRPost logging.debug("Using endpoint %s", endpoint) - x = Device(endpoint, force_protocol=protocol, debug=debug) + x = Device( + endpoint, force_protocol=protocol, debug=debug, devinfo_file=devinfo_file + ) try: await x.get_supported_methods() except SongpalException as ex: diff --git a/songpal/service.py b/songpal/service.py index bfe7a5f..2142bf8 100644 --- a/songpal/service.py +++ b/songpal/service.py @@ -73,7 +73,7 @@ async def from_payload(cls, payload, endpoint, idgen, debug, force_protocol=None protocols = payload["protocols"] _LOGGER.debug("Available protocols for %s: %s", service_name, protocols) - if force_protocol and force_protocol.value in protocols: + if force_protocol: protocol = force_protocol elif "websocket:jsonizer" in protocols: protocol = ProtocolType.WebSocket