예제 #1
0
    async def create_post_request(self, method: str, params: Dict = None):
        """Call the given method over POST.

        :param method: Name of the method
        :param params: dict of parameters
        :return: JSON object
        """
        if params is None:
            params = {}
        headers = {"Content-Type": "application/json"}
        payload = {
            "method": method,
            "params": [params],
            "id": next(self.idgen),
            "version": "1.0",
        }

        if self.debug > 1:
            _LOGGER.debug("> POST %s with body: %s", self.guide_endpoint,
                          payload)

        try:
            async with aiohttp.ClientSession(headers=headers) as session:
                res = await session.post(self.guide_endpoint,
                                         json=payload,
                                         headers=headers)
                if self.debug > 1:
                    _LOGGER.debug("Received %s: %s" % (res.status, res.text))
                if res.status != 200:
                    res_json = await res.json(content_type=None)
                    raise SongpalException(
                        "Got a non-ok (status %s) response for %s" %
                        (res.status, method),
                        error=res_json.get("error"),
                    )

                res_json = await res.json(content_type=None)
        except aiohttp.ClientConnectionError as ex:
            raise SongpalConnectionException(ex)
        except aiohttp.InvalidURL as ex:
            raise SongpalException("Unable to do POST request: %s" %
                                   ex) from ex

        if "error" in res_json:
            raise SongpalException("Got an error for %s" % method,
                                   error=res_json["error"])

        if self.debug > 1:
            _LOGGER.debug("Got %s: %s", method, pf(res_json))

        return res_json
예제 #2
0
    async def create_post_request(self, method: str, params: Dict = None):
        """Call the given method over POST.

        :param method: Name of the method
        :param params: dict of parameters
        :return: JSON object
        """
        if params is None:
            params = {}
        headers = {"Content-Type": "application/json"}
        payload = {
            "method": method,
            "params": [params],
            "id": next(self.idgen),
            "version": "1.0",
        }
        req = requests.Request("POST",
                               self.guide_endpoint,
                               data=json.dumps(payload),
                               headers=headers)
        prepreq = req.prepare()
        if self.debug > 1:
            _LOGGER.debug(
                "Sending %s %s - headers: %s body: %s" %
                (prepreq.method, prepreq.url, prepreq.headers, prepreq.body))
        s = requests.Session()
        try:
            res = s.send(prepreq)
            if self.debug > 1:
                _LOGGER.debug("Received %s: %s" % (res.status_code, res.text))
            if res.status_code != 200:
                raise SongpalException(
                    "Got a non-ok (status %s) response for %s" %
                    (res.status_code, method),
                    error=res.json()["error"])

            res = res.json()
        except requests.RequestException as ex:
            raise SongpalException("Unable to get APIs: %s" % ex) from ex

        if "error" in res:
            raise SongpalException(
                "Got an error for %s" % method,
                error=res["error"],
            )

        if self.debug > 1:
            _LOGGER.debug("Got %s: %s", method, pf(res))

        return res
예제 #3
0
 async def get_zone(self, name) -> Zone:
     zones = await self.get_zones()
     try:
         zone = next((x for x in zones if x.title == name))
         return zone
     except StopIteration:
         raise SongpalException("Unable to find zone %s" % name)
예제 #4
0
    async def create_post_request(self, method: str, params: Dict = None):
        """Call the given method over POST.

        :param method: Name of the method
        :param params: dict of parameters
        :return: JSON object
        """
        if params is None:
            params = {}
        headers = {"Content-Type": "application/json"}
        payload = {
            "method": method,
            "params": [params],
            "id": next(self.idgen),
            "version": "1.0",
        }

        if self.debug > 1:
            _LOGGER.debug("> POST %s with body: %s", self.guide_endpoint,
                          payload)

        async with aiohttp.ClientSession(headers=headers) as session:
            res = await session.post(self.guide_endpoint,
                                     json=payload,
                                     headers=headers)
            if self.debug > 1:
                _LOGGER.debug("Received %s: %s" % (res.status_code, res.text))
            if res.status != 200:
                raise SongpalException(
                    "Got a non-ok (status %s) response for %s" %
                    (res.status, method),
                    error=await res.json()["error"],
                )

            res = await res.json()

        # TODO handle exceptions from POST? This used to raise SongpalException
        #      on requests.RequestException (Unable to get APIs).

        if "error" in res:
            raise SongpalException("Got an error for %s" % method,
                                   error=res["error"])

        if self.debug > 1:
            _LOGGER.debug("Got %s: %s", method, pf(res))

        return res
예제 #5
0
    async def from_payload(cls,
                           payload,
                           endpoint,
                           idgen,
                           debug,
                           force_protocol=None):
        """Create Service object from a payload."""
        service_name = payload["service"]

        if "protocols" not in payload:
            raise SongpalException(
                "Unable to find protocols from payload: %s" % payload)

        protocols = payload["protocols"]
        _LOGGER.debug("Available protocols for %s: %s", service_name,
                      protocols)
        if force_protocol and force_protocol.value in protocols:
            protocol = force_protocol
        elif "websocket:jsonizer" in protocols:
            protocol = ProtocolType.WebSocket
        elif "xhrpost:jsonizer" in protocols:
            protocol = ProtocolType.XHRPost
        else:
            raise SongpalException("No known protocols for %s, got: %s" %
                                   (service_name, protocols))
        _LOGGER.debug("Using protocol: %s" % protocol)

        service_endpoint = "%s/%s" % (endpoint, service_name)

        # creation here we want to pass the created service class to methods.
        service = cls(service_name, service_endpoint, protocol, idgen, debug)

        await service.fetch_mehods(debug)

        if "notifications" in payload and "switchNotifications" in service.methods:
            notifications = [
                Notification(
                    service_endpoint,
                    service.methods["switchNotifications"],
                    notification,
                ) for notification in payload["notifications"]
            ]
            service.notifications = notifications
            _LOGGER.debug("Got notifications: %s" % notifications)
예제 #6
0
 async def get_zones(self) -> List[Zone]:
     """Return list of available zones."""
     res = await self.services["avContent"][
         "getCurrentExternalTerminalsStatus"]()
     zones = [
         Zone.make(services=self.services, **x) for x in res
         if "meta:zone:output" in x["meta"]
     ]
     if not zones:
         raise SongpalException("Device has no zones")
     return zones
예제 #7
0
    def __getitem__(self, item) -> Method:
        """Return a method for the given name.

        Example:
            if "setPowerStatus" in system_service:
                system_service["setPowerStatus"](status="off")

        Raises SongpalException if the method does not exist.

        """
        if item not in self._methods:
            raise SongpalException(f"{self} does not contain method {item}")
        return self._methods[item]
예제 #8
0
    async def get_inputs(self) -> List[Input]:
        """Return list of available outputs."""
        if "avContent" in self.services:
            res = await self.services["avContent"][
                "getCurrentExternalTerminalsStatus"]()
            return [
                Input.make(services=self.services, **x) for x in res
                if "meta:zone:output" not in x["meta"]
            ]
        else:
            if self._upnp_discovery is None:
                raise SongpalException(
                    "avContent service not available and UPnP fallback failed")

            return await self._get_inputs_upnp()
예제 #9
0
    async def set_power(self,
                        value: bool,
                        wol: List[str] = None,
                        get_sys_info=False):
        """Toggle the device on and off."""
        if value:
            status = "active"
        else:
            status = "off"

        if value is False and get_sys_info is True and self._sysinfo is None:
            # get sys info to be able to turn device back on
            try:
                await self.get_system_info()
            except Exception:
                pass

        try:
            if "system" in self.services:
                return await self.services["system"]["setPowerStatus"](
                    status=status)
            else:
                raise SongpalException("System service not available")
        except SongpalException as e:
            if value and (self._sysinfo or wol):
                if wol:
                    logging.debug(
                        "Sending WoL magic packet to supplied mac addresses %s",
                        wol)
                    send_magic_packet(*wol)
                    return
                if self._sysinfo:
                    logging.debug(
                        "Sending WoL magic to known mac addresses %s",
                        (self._sysinfo.macAddr, self._sysinfo.wirelessMacAddr),
                    )
                    send_magic_packet(*[
                        mac for mac in (
                            self._sysinfo.macAddr,
                            self._sysinfo.wirelessMacAddr,
                        ) if mac is not None
                    ])
                    return
            raise e
예제 #10
0
    async def _get_upnp_services(self):
        requester = AiohttpRequester()
        factory = UpnpFactory(requester)

        if self._upnp_device is None:
            self._upnp_device = await factory.async_create_device(
                self._upnp_discovery.upnp_location)

        if self._upnp_renderer is None:
            media_renderers = await DmrDevice.async_search(timeout=1)
            host = urlparse(self.endpoint).hostname
            media_renderer_location = next(
                (r["location"] for r in media_renderers
                 if urlparse(r["location"]).hostname == host),
                None,
            )
            if media_renderer_location is None:
                raise SongpalException("Could not find UPnP media renderer")

            self._upnp_renderer = await factory.async_create_device(
                media_renderer_location)
예제 #11
0
    async def _get_system_info(self) -> Sysinfo:
        """Return system information including mac addresses and current version."""

        if self.services["system"].has_method("getSystemInformation"):
            return Sysinfo.make(
                **await self.services["system"]["getSystemInformation"]())
        elif self.services["system"].has_method("getNetworkSettings"):
            info = await self.services["system"]["getNetworkSettings"](netif=""
                                                                       )

            def get_addr(info, iface):
                addr = next((i for i in info if i["netif"] == iface),
                            {}).get("hwAddr")
                return addr.lower().replace("-", ":") if addr else addr

            macAddr = get_addr(info, "eth0")
            wirelessMacAddr = get_addr(info, "wlan0")
            version = self._upnp_discovery.version if self._upnp_discovery else None
            return Sysinfo.make(macAddr=macAddr,
                                wirelessMacAddr=wirelessMacAddr,
                                version=version)
        else:
            raise SongpalException("getSystemInformation not supported")
예제 #12
0
    async def create_post_request(self, method, params):
        headers = {"Content-Type": "application/json"}
        payload = {"method": method,
                   "params": [params],
                   "id": next(self.idgen),
                   "version": "1.0"}
        req = requests.Request("POST", self.guide_endpoint,
                               data=json.dumps(payload), headers=headers)
        prepreq = req.prepare()
        s = requests.Session()
        try:
            response = s.send(prepreq)
            if response.status_code != 200:
                _LOGGER.error("Got !200 response: %s" % response.text)
                return None

            response = response.json()
        except requests.RequestException as ex:
            raise SongpalException("Unable to get APIs: %s" % ex) from ex

        if self.debug > 1:
            _LOGGER.debug("Got getSupportedApiInfo: %s", pf(response))

        return response
예제 #13
0
    async def from_payload(cls,
                           payload,
                           endpoint,
                           idgen,
                           debug,
                           force_protocol=None):
        """Create Service object from a payload."""
        service_name = payload["service"]

        if "protocols" not in payload:
            raise SongpalException(
                "Unable to find protocols from payload: %s" % payload)

        protocols = payload["protocols"]
        _LOGGER.debug("Available protocols for %s: %s", service_name,
                      protocols)
        if force_protocol and force_protocol.value in protocols:
            protocol = force_protocol
        elif "websocket:jsonizer" in protocols:
            protocol = ProtocolType.WebSocket
        elif "xhrpost:jsonizer" in protocols:
            protocol = ProtocolType.XHRPost
        else:
            raise SongpalException("No known protocols for %s, got: %s" %
                                   (service_name, protocols))
        _LOGGER.debug("Using protocol: %s" % protocol)

        service_endpoint = "%s/%s" % (endpoint, service_name)

        # creation here we want to pass the created service class to methods.
        service = cls(service_name, service_endpoint, protocol, idgen, debug)

        sigs = await cls.fetch_signatures(service_endpoint, protocol, idgen)

        if debug > 1:
            _LOGGER.debug("Signatures: %s", sigs)
        if "error" in sigs:
            _LOGGER.error("Got error when fetching sigs: %s", sigs["error"])
            return None

        methods = {}

        for sig in sigs["results"]:
            name = sig[0]
            parsed_sig = MethodSignature.from_payload(*sig)
            if name in methods:
                _LOGGER.warning(
                    "Got duplicate signature for %s, existing was %s. Keeping the existing one",
                    parsed_sig, methods[name])
            else:
                methods[name] = Method(service, parsed_sig, debug)

        service.methods = methods

        if "notifications" in payload and "switchNotifications" in methods:
            notifications = [
                Notification(service_endpoint, methods["switchNotifications"],
                             notification)
                for notification in payload["notifications"]
            ]
            service.notifications = notifications
            _LOGGER.debug("Got notifications: %s" % notifications)

        return service
예제 #14
0
    async def call_method(self, method, *args, **kwargs):
        """Call a method (internal).

        This is an internal implementation, which formats the parameters if necessary
         and chooses the preferred transport protocol.
         The return values are JSON objects.
        Use :func:__call__: provides external API leveraging this.
        """
        _LOGGER.debug("%s got called with args (%s) kwargs (%s)" %
                      (method.name, args, kwargs))

        # Used for allowing keeping reading from the socket
        _consumer = None
        if "_consumer" in kwargs:
            if self.active_protocol != ProtocolType.WebSocket:
                raise SongpalException(
                    "Notifications are only supported over websockets")
            _consumer = kwargs["_consumer"]
            del kwargs["_consumer"]

        if len(kwargs) == 0 and len(args) == 0:
            params = []  # params need to be empty array, if none is given
        elif len(kwargs) > 0:
            params = [kwargs]
        elif len(args) == 1 and args[0] is not None:
            params = [args[0]]
        else:
            params = []

        # TODO check for type correctness
        # TODO note parameters are not always necessary, see getPlaybackModeSettings
        # which has 'target' and 'uri' but works just fine without anything (wildcard)
        # if len(params) != len(self._inputs):
        #    _LOGGER.error("args: %s signature: %s" % (args,
        #                                              self.signature.input))
        #    raise Exception("Invalid number of inputs, wanted %s got %s / %s" % (
        #        len(self.signature.input), len(args), len(kwargs)))

        async with aiohttp.ClientSession() as session:
            req = {
                "method": method.name,
                "params": params,
                "version": method.version,
                "id": next(self.idgen),
            }
            if self.debug > 1:
                _LOGGER.debug("sending request: %s (proto: %s)", req,
                              self.active_protocol)
            if self.active_protocol == ProtocolType.WebSocket:
                async with session.ws_connect(self.endpoint,
                                              timeout=self.timeout,
                                              heartbeat=self.timeout * 5) as s:
                    await s.send_json(req)
                    # If we have a consumer, we are going to loop forever while
                    # emiting the incoming payloads to e.g. notification handler.
                    if _consumer is not None:
                        while True:
                            res_raw = await s.receive_json()
                            res = self.wrap_notification(res_raw)
                            _LOGGER.debug("Got notification: %s", res)
                            if self.debug > 1:
                                _LOGGER.debug("Got notification raw: %s",
                                              res_raw)

                            await _consumer(res)

                    res = await s.receive_json()
                    return res
            else:
                res = await session.post(self.endpoint, json=req)
                return await res.json()
예제 #15
0
    async def from_payload(cls,
                           payload,
                           endpoint,
                           idgen,
                           debug,
                           force_protocol=None):
        service = payload["service"]
        methods = {}

        if 'protocols' not in payload:
            raise SongpalException(
                "Unable to find protocols from payload: %s" % payload)

        protocols = payload['protocols']
        _LOGGER.debug("Available protocols for %s: %s", service, protocols)
        if force_protocol and force_protocol.value in protocols:
            protocol = force_protocol
        elif 'websocket:jsonizer' in protocols:
            protocol = ProtocolType.WebSocket
        elif 'xhrpost:jsonizer' in protocols:
            protocol = ProtocolType.XHRPost
        else:
            raise SongpalException("No known protocols for %s, got: %s" %
                                   (service, protocols))
        _LOGGER.debug("Using protocol: %s" % protocol)

        versions = set()
        for method in payload['apis']:
            # TODO we take only the first version here per method
            # should we prefer the newest version instead of that?
            if len(method["versions"]) == 0:
                _LOGGER.warning("No versions found for %s", method)
            elif len(method["versions"]) > 1:
                _LOGGER.warning(
                    "More than on version for %s, "
                    "using the first one", method)
            versions.add(method["versions"][0]["version"])

        service_endpoint = "%s/%s" % (endpoint, service)

        signatures = {}
        for version in versions:
            sigs = await cls.fetch_signatures(service_endpoint, version,
                                              protocol, idgen)

            if debug > 1:
                _LOGGER.debug("Signatures: %s", sigs)
            if 'error' in sigs:
                _LOGGER.error("Got error when fetching sigs: %s",
                              sigs['error'])
                return None

            for sig in sigs["results"]:
                signatures[sig[0]] = Signature(*sig)

        for method in payload["apis"]:
            name = method["name"]
            if name in methods:
                raise SongpalException("Got duplicate %s for %s" %
                                       (name, endpoint))
            if name not in signatures:
                _LOGGER.debug("Got no signature for %s on %s" %
                              (name, endpoint))
                continue
            methods[name] = Method(service, service_endpoint, method,
                                   signatures[name], protocol, idgen, debug)

        notifications = []
        # TODO switchnotifications check is broken?
        if "notifications" in payload and "switchNotifications" in methods:
            notifications = [
                Notification(service_endpoint, methods["switchNotifications"],
                             notification)
                for notification in payload["notifications"]
            ]

        return cls(service, methods, notifications, protocols, idgen)
예제 #16
0
 def __getitem__(self, item) -> Method:
     if item not in self._methods:
         raise SongpalException("%s does not contain method %s" %
                                (self, item))
     return self._methods[item]