def dlna_handle_notify_last_change(state_var: UpnpStateVariable) -> None: """ Handle changes to LastChange state variable. This expands all changed state variables in the LastChange state variable. Note that the callback is called twice: - for the original event; - for the expanded event, via this function. """ if state_var.name != "LastChange": raise UpnpError("Call this only on state variable LastChange") event_data: Optional[str] = state_var.value if not event_data: _LOGGER.debug("No event data on state_variable") return changes = _parse_last_change_event(event_data) if "0" not in changes: _LOGGER.warning("Only InstanceID 0 is supported") return service = state_var.service changes_0 = changes["0"] service.notify_changed_state_variables(changes_0)
async def async_set_transport_uri(self, media_url: str, media_title: str, meta_data: Optional[str] = None) -> None: """Play a piece of media.""" # pylint: disable=too-many-arguments # escape media_url _LOGGER.debug("Set transport uri: %s", media_url) media_url_parts = urlparse(media_url) media_url = urlunparse([ media_url_parts.scheme, media_url_parts.netloc, media_url_parts.path, "", quote_plus(media_url_parts.query), "", ]) # queue media if not meta_data: meta_data = await self.construct_play_media_metadata( media_url, media_title) action = self._action("AVT", "SetAVTransportURI") if not action: raise UpnpError("Missing action AVT/SetAVTransportURI") await action.async_call(InstanceID=0, CurrentURI=media_url, CurrentURIMetaData=meta_data)
def dlna_handle_notify_last_change(state_var: UpnpStateVariable): """ Handle changes to LastChange state variable. This expands all changed state variables in the LastChange state variable. Note that the callback is called twice: - for the original event; - for the expanded event, via this function. """ if state_var.name != 'LastChange': raise UpnpError('Call this only on state variable LastChange') service = state_var.service changes = {} el_event = ET.fromstring(state_var.value) for el_instance in el_event: if not el_instance.tag.endswith("}InstanceID"): continue if el_instance.attrib['val'] != '0': _LOGGER.warning('Only InstanceID 0 is supported') continue for el_state_var in el_instance: name = el_state_var.tag.split('}')[1] value = el_state_var.attrib['val'] changes[name] = value service.notify_changed_state_variables(changes)
async def async_set_transport_uri( self, media_url: str, media_title: str, mime_type: str, upnp_class: str, override_mime_type: Optional[str] = None, override_dlna_features: Optional[str] = None) -> None: """Play a piece of media.""" # pylint: disable=too-many-arguments # escape media_url _LOGGER.debug('Set transport uri: %s', media_url) media_url_parts = urlparse(media_url) media_url = urlunparse([ media_url_parts.scheme, media_url_parts.netloc, media_url_parts.path, '', quote_plus(media_url_parts.query), '' ]) # queue media meta_data = await self._construct_play_media_metadata( media_url, media_title, mime_type, upnp_class, override_mime_type=override_mime_type, override_dlna_features=override_dlna_features) action = self._action('AVT', 'SetAVTransportURI') if not action: raise UpnpError('Missing action AVT/SetAVTransportURI') await action.async_call(InstanceID=0, CurrentURI=media_url, CurrentURIMetaData=meta_data)
async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" action = self._action("RC", "SetMute") if not action: raise UpnpError("Missing action RC/SetMute") desired_mute = bool(mute) await action.async_call(InstanceID=0, Channel="Master", DesiredMute=desired_mute)
async def async_mute_volume(self, mute: bool) -> None: """Mute the volume.""" action = self._action('RC', 'SetMute') if not action: raise UpnpError('Missing action RC/SetMute') desired_mute = bool(mute) await action.async_call(InstanceID=0, Channel='Master', DesiredMute=desired_mute)
async def async_stop(self) -> None: """Send stop command.""" if not self._can_transport_action("stop"): _LOGGER.debug("Cannot do Stop") return action = self._action("AVT", "Stop") if not action: raise UpnpError("Missing action AVT/Stop") await action.async_call(InstanceID=0)
async def async_play(self) -> None: """Send play command.""" if not self._can_transport_action("play"): _LOGGER.debug("Cannot do Play") return action = self._action("AVT", "Play") if not action: raise UpnpError("Missing action AVT/Play") await action.async_call(InstanceID=0, Speed="1")
async def async_next(self) -> None: """Send next track command.""" if not self._can_transport_action("next"): _LOGGER.debug("Cannot do Next") return action = self._action("AVT", "Next") if not action: raise UpnpError("Missing action AVT/Next") await action.async_call(InstanceID=0)
async def async_play(self) -> None: """Send play command.""" if not self._can_transport_action('play'): _LOGGER.debug('Cannot do Play') return action = self._action('AVT', 'Play') if not action: raise UpnpError('Missing action AVT/Play') await action.async_call(InstanceID=0, Speed='1')
async def async_next(self) -> None: """Send next track command.""" if not self._can_transport_action('next'): _LOGGER.debug('Cannot do Next') return action = self._action('AVT', 'Next') if not action: raise UpnpError('Missing action AVT/Next') await action.async_call(InstanceID=0)
async def async_previous(self) -> None: """Send previous track command.""" if not self._can_transport_action('previous'): _LOGGER.debug('Cannot do Previous') return action = self._action('AVT', 'Previous') if not action: raise UpnpError('Missing action AVT/Previous') await action.async_call(InstanceID=0)
async def async_stop(self) -> None: """Send stop command.""" if not self._can_transport_action('stop'): _LOGGER.debug('Cannot do Stop') return action = self._action('AVT', 'Stop') if not action: raise UpnpError('Missing action AVT/Stop') await action.async_call(InstanceID=0)
async def async_previous(self) -> None: """Send previous track command.""" if not self._can_transport_action("previous"): _LOGGER.debug("Cannot do Previous") return action = self._action("AVT", "Previous") if not action: raise UpnpError("Missing action AVT/Previous") await action.async_call(InstanceID=0)
async def async_seek_rel_time(self, time: timedelta) -> None: """Send seek command with REL_TIME.""" if not self._can_transport_action("seek"): _LOGGER.debug("Cannot do Seek by REL_TIME") return action = self._action("AVT", "Seek") if not action: raise UpnpError("Missing action AVT/Seek") target = time_to_str(time) await action.async_call(InstanceID=0, Unit="REL_TIME", Target=target)
async def async_seek_abs_time(self, time: timedelta) -> None: """Send seek command with ABS_TIME.""" if not self._can_transport_action('seek'): _LOGGER.debug('Cannot do Seek by ABS_TIME') return action = self._action('AVT', 'Seek') if not action: raise UpnpError('Missing action AVT/Seek') target = time_to_str(time) await action.async_call(InstanceID=0, Unit='ABS_TIME', Target=target)
def _level(self, var_name: str) -> Optional[float]: state_var = self._state_variable("RC", var_name) if state_var is None: raise UpnpError("Missing StateVariable RC/%s" % (var_name, )) value: Optional[float] = state_var.value if value is None: _LOGGER.debug("Got no value for %s", var_name) return None max_value = state_var.max_value or 100.0 return min(value / max_value, 1.0)
async def _async_set_level(self, var_name: str, level: float, **kwargs: Any) -> None: action = self._action("RC", "Set%s" % var_name) if not action: raise UpnpError("Missing Action RC/Set%s" % (var_name, )) arg_name = "Desired%s" % var_name argument = action.argument(arg_name) if not argument: raise UpnpError("Missing Argument %s for Action RC/Set%s" % ( arg_name, var_name, )) state_variable = argument.related_state_variable min_ = state_variable.min_value or 0 max_ = state_variable.max_value or 100 desired_level = int(min_ + level * (max_ - min_)) args = kwargs.copy() args[arg_name] = desired_level await action.async_call(InstanceID=0, **args)
def dlna_handle_notify_last_change(state_var): """ Handle changes to LastChange state variable. This expands all changed state variables in the LastChange state variable. Note that the callback is called twice: - for the original event; - for the expanded event, via this function. """ if state_var.name != 'LastChange': raise UpnpError('Call this only on state variable LastChange') service = state_var.service changed_state_variables = [] el_event = ET.fromstring(state_var.value) _LOGGER.debug("Event payload: %s" % state_var.value) for el_instance in el_event: if not el_instance.tag.endswith("}InstanceID"): continue if el_instance.attrib['val'] != '0': _LOGGER.warning('Only InstanceID 0 is supported') continue for el_state_var in el_instance: name = el_state_var.tag.split('}')[1] state_var = service.state_variable(name) if state_var is None: _LOGGER.debug("State variable %s does not exist, ignoring", name) continue value = el_state_var.attrib['val'] try: state_var.upnp_value = value except vol.error.MultipleInvalid: _LOGGER.error('Got invalid value for %s: %s', state_var, value) changed_state_variables.append(state_var) service.notify_changed_state_variables(changed_state_variables)
async def _construct_play_media_metadata( self, media_url: str, media_title: str, mime_type: str, upnp_class: str, override_mime_type: Optional[str] = None, override_dlna_features: Optional[str] = None, ) -> str: """ Construct the metadata for play_media command. This queries the source and takes mime_type/dlna_features from it. :arg media_url URL to media :arg media_title :arg mime_type Suggested mime type, will be overridden by source if possible :arg upnp_class UPnP class :arg override_mime_type Enfore mime_type, even if source reports a different mime_type :arg override_dlna_features Enforce DLNA features, even if source reports different features :return String containing metadata """ # pylint: disable=too-many-arguments, too-many-locals media_info = { "mime_type": mime_type, "dlna_features": "*", } # do a HEAD/GET, to retrieve content-type/mime-type try: headers = await self._fetch_headers( media_url, {"GetContentFeatures.dlna.org": "1"}) if headers: if "Content-Type" in headers: media_info["mime_type"] = headers["Content-Type"] if "ContentFeatures.dlna.org" in headers: media_info["dlna_features"] = headers[ "contentFeatures.dlna.org"] except Exception: # pylint: disable=broad-except pass # use CM/GetProtocolInfo to improve on dlna_features if self.has_get_protocol_info: protocol_info_entries = ( await self._async_get_sink_protocol_info_for_mime_type( media_info["mime_type"])) for entry in protocol_info_entries: if entry[3] == "*": # device accepts anything, send this media_info["dlna_features"] = "*" # allow overriding of mime_type/dlna_features if override_mime_type: media_info["mime_type"] = override_mime_type if override_dlna_features: media_info["dlna_features"] = override_dlna_features # build DIDL-Lite item + resource didl_item_type = didl_lite.type_by_upnp_class(upnp_class) if not didl_item_type: raise UpnpError("Unknown DIDL-lite type") protocol_info = "http-get:*:{mime_type}:{dlna_features}".format( **media_info) resource = didl_lite.Resource(uri=media_url, protocol_info=protocol_info) item = didl_item_type( id="0", parent_id="-1", title=media_title, restricted="false", resources=[resource], ) xml_string: bytes = didl_lite.to_xml_string(item) return xml_string.decode("utf-8")
async def construct_play_media_metadata( self, media_url: str, media_title: str, default_mime_type: Optional[str] = None, default_upnp_class: Optional[str] = None, override_mime_type: Optional[str] = None, override_upnp_class: Optional[str] = None, override_dlna_features: Optional[str] = None, ) -> str: """ Construct the metadata for play_media command. This queries the source and takes mime_type/dlna_features from it. :arg media_url URL to media :arg media_title :arg default_mime_type Suggested mime type, will be overridden by source if possible :arg default_upnp_class Suggested UPnP class, will be used as fallback for autodetection :arg override_mime_type Enforce mime_type, even if source reports a different mime_type :arg override_upnp_class Enforce upnp_class, even if autodetection finds something usable :arg override_dlna_features Enforce DLNA features, even if source reports different features :return String containing metadata """ # pylint: disable=too-many-arguments, too-many-locals, too-many-branches mime_type = override_mime_type or "" upnp_class = override_upnp_class or "" dlna_features = override_dlna_features or "*" if None in (override_mime_type, override_dlna_features): # do a HEAD/GET, to retrieve content-type/mime-type try: headers = await self._fetch_headers( media_url, {"GetContentFeatures.dlna.org": "1"}) if headers: if not override_mime_type and "Content-Type" in headers: mime_type = headers["Content-Type"] if (not override_dlna_features and "ContentFeatures.dlna.org" in headers): dlna_features = headers["contentFeatures.dlna.org"] except Exception: # pylint: disable=broad-except pass if not mime_type: _type = guess_type(media_url.split("?")[0]) mime_type = _type[0] or "" if not mime_type: mime_type = default_mime_type or "application/octet-stream" # use CM/GetProtocolInfo to improve on dlna_features if (not override_dlna_features and dlna_features != "*" and self.has_get_protocol_info): protocol_info_entries = ( await self._async_get_sink_protocol_info_for_mime_type(mime_type) ) for entry in protocol_info_entries: if entry[3] == "*": # device accepts anything, send this dlna_features = "*" # Try to derive a basic upnp_class from mime_type if not override_upnp_class: mime_type = mime_type.lower() for _mime, _class in MIME_TO_UPNP_CLASS_MAPPING.items(): if mime_type.startswith(_mime): upnp_class = _class break else: upnp_class = default_upnp_class or "object.item" # build DIDL-Lite item + resource didl_item_type = didl_lite.type_by_upnp_class(upnp_class) if not didl_item_type: raise UpnpError("Unknown DIDL-lite type") protocol_info = f"http-get:*:{mime_type}:{dlna_features}" resource = didl_lite.Resource(uri=media_url, protocol_info=protocol_info) item = didl_item_type( id="0", parent_id="-1", title=media_title, restricted="false", resources=[resource], ) xml_string: bytes = didl_lite.to_xml_string(item) return xml_string.decode("utf-8")