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
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
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)
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
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)
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
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]
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()
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
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)
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")
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
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
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()
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)
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]