def get_preset_arguments(self, source, preset_num): ''' Returns a dictionary with the preset information. Args: source (:class:`Source`): the source with the preset information preset_num (int): the preset number to retrieve ''' args = {} if source.mcid == 'net_radio': args['band'] = '' else: # source.mcid not 'net_radio' raise mcx.LogicError(''.join(('Source ', source.mcid, ' does not have presets.'))) if preset_num < 1 or preset_num > self._max_presets: raise mcx.LogicError(''.join(('Preset ', str(preset_num), ' is out of range.'))) args['preset_num'] = str(preset_num) return args
def is_zone_id(self, zone_id=None, zone_mcid=None, raises=False): ''' Returns True if the id corresponds to the current zone. Args: zone_id (string): zone id zone_mcid (string): zone MusicCast id raises (boolean): if True, an exception is raised if result is False ''' if zone_id is not None: if self.id == zone_id: return True msg_id = zone_id elif zone_mcid is not None: if self.device.is_mcready(raises=False): if self.mcid == zone_mcid: return True msg_id = zone_mcid else: # request to check against mcid on non MusicCast Ready device if raises: raise mcx.LogicError( 'Can not determine MusicCast id on non-MusicCast Ready device' ) else: return False else: if raises: raise mcx.ConfigError( 'No valid Zone id argument to check against.') else: return False if raises: raise mcx.ConfigError(''.join( ('Zone id ', msg_id, ' does not match this zone.'))) else: return False
def _filter_topics(self): ''' docstring ''' _logger.debug('Filtering topics') if not self._msgin.iscmd: # ignore status messages (for now?) return False if self._msgin.sender == 'musiccast': # ignore echoes return False # the following filters could be dealt by subscriptions if not self._msgin.function and not self._msgin.gateway: raise mcx.LogicError('No function or gateway in message.') if self._msgin.gateway and self._msgin.gateway != 'musiccast': return False if self._msgin.function and self._msgin.function != 'AudioVideo': return False if not self._msgin.location and not self._msgin.device: raise mcx.LogicError('No location or device in message.') return True
def update_preset_info(self): ''' Retrieves the preset_info structure. The `getPresetInfo` request involves only types **tuner** and **netusb**. Treatment in either case is different, see the Yamaha doc for details. This method is supposed to be overridden in both cases. ''' raise mcx.LogicError(''.join(('Type <', self.type, '> does not have preset info.')))
def get_preset_arguments(self, source, preset_num): ''' Returns a dictionary with the preset information. Args: source (:class:`Source`): the source with the preset information preset_num (int): the preset number to retrieve ''' raise mcx.LogicError(''.join(('Source ', source.mcid, ' does not have presets.')))
def update_play_message(self, value): ''' Updates the play_message attribute with the new value. This event only applies to the **netusb** group. Args: value (string): the new value of play_message. ''' raise mcx.LogicError(''.join(('Type <', self.type, '> does not have play message info.')))
def _resolve_zone(self): ''' docstring''' _logger.debug('Resolve zone') # find the zone to operate by resolving the 'address' self._explicit = False zone_from_location = None zone_from_device = None # if self._msgin.gateway: self._explicit = True if self._msgin.location: try: zone_from_location = self._locations[self._msgin.location] except KeyError: raise mcx.LogicError(''.join( ('Location <', self._msgin.location, '>not found.'))) if self._msgin.device: self._explicit = True try: device = self._devices_by_id[self._msgin.device] except KeyError: raise mcx.LogicError(''.join( ('Device <', self._msgin.device, '> not found'))) # check if the zone in the device is provided in the arguments zone_id = self._msgin.arguments.get('zone', None) if zone_id: zone_from_device = device.get_zone(zone_id=zone_id, raises=True) # if there was no zone specified in the arguments, zone_from_device is still None here # check consistency between device and location, if location was resolved if zone_from_location: if zone_from_location.device != device: raise mcx.LogicError( 'Location and device point to different devices.') if zone_from_device and zone_from_location != zone_from_device: raise mcx.LogicError( 'Location and device point to different zones.') return zone_from_location # all other cases lead to this else: if not zone_from_device: zone_from_device = device.zones[ 0] # take the first one by default return zone_from_device return zone_from_location
def get_argument(self, arg): ''' Retrieves argument from arguments dictionary. Args: arg (string): the name of the argument sought ''' try: return self._arguments[arg] except KeyError: raise mcx.LogicError(''.join(('No argument <', arg, '> found.')))
def update_play_time(self, value): ''' Updates the play_time attribute with the new value. Only concerns MusicCast types **cd** and **netusb**. The **play_time** event get sent every second by MusicCast devices once a cd or a streaming service starts playing. Args: value (integer in string form): the new value of play_time. ''' raise mcx.LogicError(''.join(('Type <', self.type, '> does not have play time info.')))
def _transform_arg(self, key, invalue=None, mcvalue=None): '''Transforms a message argument from/to internal to/from MusicCast. Args: key (string): internal name of argument. invalue: the internal value to be transformed; if provided the transformation is done from this value to the MusicCast value, which is returned. mcvalue: the MusicCast value to be transformed; relevant only if ``invalue`` is None, in which case the transformation is done from this value to the internal value, which is returned. Returns: string: the transformed representation of the value. ''' # TODO: do we need to transform the keys as well? try: func = TRANSFORM_ARG[key] # Retrieve the transformation lambdas except KeyError: raise mcx.LogicError(''.join( ('Argument ', str(key), ' has no transformation.'))) if invalue is not None: # transform from internal to MusicCast value = invalue trsfrm = 0 elif mcvalue is not None: # transform from MusicCast to internal value = mcvalue trsfrm = 1 else: raise mcx.ConfigError(''.join( ('No valid parameters to transform <', str(key), '> on zone <', str(self.id), '> of device <', str(self.device.id), '>.'))) try: trsfrm_value = func[trsfrm](self, value) except (TypeError, ValueError) as err: # errors to catch in case of bad format raise mcx.LogicError(''.join( ('Value ', str(value), ' of argument ', str(key), ' seems of the wrong type. Error:\n\t', str(err)))) return trsfrm_value
def set_preset(self, src_id=None): '''Set the preset specified in the arguments for the source. Args: action (string): the action to send to the MusicCast device. source_id (string): the internal keyword of the source to be preset, if supplied, otherwise it is expected to be in the arguments. It can only be **tuner** or **netusb**. ''' # Find which zone (remote or not) we need to control to execute this command ctrl_zone = self._get_current_input(raises=True).get_control_zone() # Check if it is MusicCast Ready, otherwise there is nothing to do ctrl_zone.device.is_mcready(raises=True) ctrl_zone.is_power_on(raises=True) # Resolve the source and check it is the one playing if src_id is None: src_id = self.device.system.get_argument('source') src = ctrl_zone._get_current_input() if src_id != src.id: raise mcx.LogicError(''.join( ('Can' 't preset <', src_id, '> while device <', ctrl_zone.device.id, ' is playing input <', src.id, '>.'))) # Retrieve the number of the preset. try: preset_num = int(self.device.system.get_argument('preset')) except (KeyError, ValueError): raise mcx.LogicError('No valid preset argument found.') # The command format depends on the source qualifier = src.playinfo_type.type args = src.playinfo_type.get_preset_arguments(src, preset_num) cmdtxt = 'recallPreset?zone={}&band={}&num={}'.format( ctrl_zone.mcid, args['band'], args['preset_num']) # Send the command ctrl_zone.device.conn.mcrequest(qualifier, cmdtxt) #ctrl_zone.send_reply('OK', ''.join(('preset ', src.mcid, # ' to number ', str(preset_num)))) return
def get_preset_arguments(self, source, preset_num): ''' Returns a dictionary with the preset information. Args: source (:class:`Source`): the source with the preset information preset_num (int): the preset number to retrieve ''' args = {} if self._preset_separate: args['band'] = 'dab' # for now that's the only preset we want to use. # TODO: include other bands selection. else: args['band'] = 'common' if preset_num < 1 or preset_num > self._max_presets: raise mcx.LogicError(''.join(('Preset ', str(preset_num), ' is out of range.'))) args['preset_num'] = str(preset_num) return args
def __init__(self, device): super(Tuner, self).__init__('tuner', device) # Resolve the _preset_separate and the _info_bands preset_type = self.device.get_feature(('tuner', 'preset', 'type')) if preset_type == 'separate': self._preset_separate = True else: self._preset_separate = False if self._preset_separate: func_list = self.device.get_feature(('tuner', 'func_list')) self._info_bands = [band for band in func_list if band in ('am', 'fm', 'dab')] # load the max_preset try: self._max_presets = int(self.device.get_feature(('tuner', 'preset', 'num'))) except ValueError: raise mcx.LogicError('getFeatures item <max_presets> not an int.') # Load the preset_info self._preset_info = None self.update_preset_info() return
def is_musiccast(self, raises=False): ''' Tests if the device is MusicCast. Always returns True if the device is MusicCast. Args: raises (boolean): if True, raises an exception when device is not MusicCast, otherwise it just returns False. Returns: boolean: True if device is MusicCast, False if not and `raises` is False. Raises: LogicError: if the device is not ready and the `raises` argument is True. ''' if self.musiccast: return True elif raises: raise mcx.LogicError(''.join(('The device ', self.id, ' is not MusicCast.'))) else: return False
def execute_action(self, action): ''' Executes the action in the zone provided Args: action (string): action name in internal vocabulary ''' _logger.debug(''.join( ('Execute action <', action, '> on zone <', self.id, '> of device <', self.device.id, '>.'))) self._response = '' self._reason = '' # retrieve the function to execute for this action try: func = ACTIONS[action] except KeyError: # the action is not found raise mcx.LogicError(''.join(('Action ', action, ' not found.'))) # execute the function in the zone func(self) # Return reply return self._response, self._reason
def get_input(self, input_id=None, input_mcid=None, raises=False): ''' Returns the :class:`Input` object from its id or mcid. If input_id is present, then it is used, otherwise input_mcid is used. Args: zone_id (string): the id of the zone searched zone_mcid (string): the MusicCast id of the zone searched raises (boolean): if True, raises an exception instead of returning ``False`` Returns: :class:`Input` object if found, or ``None`` Raises: ConfigError or LogicError. ''' if input_id is not None: try: return self._inputs_d[input_id] except KeyError: if raises: raise mcx.ConfigError(''.join(('Input <', input_id, '> not found in device <', self.id, '>.'))) else: return None elif input_mcid is not None: if not self.musiccast: if raises: raise mcx.LogicError(''.join(('Can not find MusicCast input <', input_mcid, '> on non-MusicCast device <', self.id, '>.'))) else: return None # TODO: make a dictionary to find the MusicCast input out of its MusicCast id. for inp in self.inputs: if inp.mcid == input_mcid: return inp if raises: raise mcx.ConfigError(''.join(('MusicCast input <', input_mcid, '> not found in device <', self.id, '>.'))) else: return None else: # both ids are None if raises: raise mcx.ConfigError(''.join(('No valid arguments in get_input() on device <', self.id, '>.'))) else: return None
def is_power_on(self, raises=False): ''' Helper function to test if power of zone is ON. Always returns True if the zone is ON. Args: raises (boolean): if True, raises an exception when zone is OFF, otherwise it just returns False. Returns: boolean: True if zone is ON, False if not and `raises` is False. Raises: LogicError: if the zone is OFF and the `raises` argument is True. ''' self.device.is_mcready(raises=True) if self._power: return True elif raises: raise mcx.LogicError(''.join( ('The zone ', self.id, ' of device ', self.device.id, ' is not turned on.'))) else: return False
def set_volume(self, up=None): ''' Sets the volume of the zone. Args: up (boolean): if given defines if volume is stepped up or down, if not then the volume to set has to be in the arguments. ''' self.device.is_mcready(raises=True) self.is_power_on(raises=True) if up is None: # retrieve the volume in the arguments; cast it to int just in case it's a string try: volume = int(self.device.system.get_argument('volume')) except (TypeError, ValueError): raise mcx.LogicError('Invalid volume argument') mc_volume = self._transform_arg('volume', invalue=volume) mc_volume = min(max(mc_volume, self._volume_min), (self._volume_min + self._volume_range)) self.device.conn.mcrequest( self.mcid, ''.join(('setVolume?volume=', str(mc_volume)))) else: self.device.conn.mcrequest( self.mcid, ''.join( ('setVolume?volume=', 'up' if up else 'down'))) # calculate volume level to update locally mc_volume = self._transform_arg('volume', invalue=self._volume) # mc_volume is an int mc_volume += (1 if up else -1) * self._volume_step mc_volume = min(max(mc_volume, self._volume_min), (self._volume_min + self._volume_range)) volume = self._transform_arg('volume', mcvalue=mc_volume) self.update_volume(volume) self.status_requested = True self._response = 'OK' self._reason = ''.join(('volume is ', str(volume))) return
def set_playback(self, action, src_id=None): '''Triggers the specified play-back action. To be able to play a source, it has to be selected first, so that the attribute `zonesource` is defined properly. The zone `zonesource` is expected to be MusicCast otherwise nothing can be done anyway. Args: action (string): the action to send to the MusicCast device. src_id (string): the internal keyword of the source to be played, if supplied, otherwise it is expected to be in the arguments. ''' # Find which zone (remote or not) we need to control to execute this command control_zone = self._get_current_input().get_control_zone() # Check if it is MusicCast Ready, otherwise there is nothing to do control_zone.device.is_mcready(raises=True) control_zone.is_power_on(raises=True) # Resolve the source and check it is the one playing if src_id is None: src_id = self.device.system.get_argument('source') source = control_zone._get_current_input() if src_id != source.id: raise mcx.LogicError(''.join( ('Can not operate source <', src_id, '> while device <', control_zone.device.id, '> is playing <', source.id, '>.'))) # Transform action mcaction = self._transform_arg('action', invalue=action) # Send command control_zone.device.conn.mcrequest( source.playinfo_type.type, ''.join( ('setPlayback?playback=', mcaction))) #control_zone.send_reply('OK', ''.join(('playback set to ', action))) return
def set_source(self, src_id=None): ''' Sets the source for the current zone, if available. Args: src_id (string): source keyword in internal vocabulary. This command is complex and involves a lot of decision making if it needs to take into account the most diverse set-ups. In most cases, every amplified zone will only have one choice per source, and if that source is unavailable for any reason (because the device it comes from is playing another source for another zone, for example), then there is nothing else to do and the command fails. But this method wants also to take care of the more complicated cases, where a zone has multiple options to select a given source, so that if one is unavailable it can choose another one. Also, this command has to deal with the case where the zone making the call is not a MusicCast one. That is because the source it will connect to might be MusicCast and has to be processed by this command. Therefore, all the following cases are possible: - zone and source are non MusicCast devices: do nothing; - zone and source are on same MusicCast device: easy; - zone is MusicCast but source is not: less easy; - zone and source are different but both MusicCast: a bit tricky; - zone is not MusicCast but source is: a bit of a pain... Finally, dealing with the source is complicated by the fact that the command should not grab the source requested without checking first if it is already used by another zone. 15May2018: Priorities in finding source: 1) Prefer same device (even if not MusicCast) 2) Prefer MusicCast devices as remote sources 3) Prefer source being already played and join it 4) Take a non MusicCast device if found 5) Prefer source being already played and join it ''' # Find src_id in the arguments dictionary if it is not given as a method argument if src_id is None: src_id = self.device.system.get_argument('source') _logger.debug(''.join(('set_source - set src_id = ', str(src_id)))) # Priority 1: find the source on the same device. # The search is made on the internal keyword, not the MusicCast one, # as we might be on a non MusicCast zone. if src_id in self.device.sources_by_id( ): # source found in same device _logger.debug(''.join( ('set_source - source found on same device ', self.device.id))) if not self.device.is_mcready(): # not MusicCast ready, abandon. raise mcx.LogicError(''.join( ('Cannot set source ', src_id, ' on device ', self.device.id, '.'))) src = self.device.sources_by_id()[src_id] # FIXME: not great... self.set_input(src.mcid) #;self._update_usedby_lists(src) return _logger.debug('set_source - source not found on same device') # Source not found on the same device. # Look for source in all *remote* devices connected to the feeds. # Priority 2: prefer MusicCast devices. # Priority 2a: join a source that is already playing. remote_zone_found = False for feed in self.device.feeds: # look through every feed for MusicCast devices with the right source remote_dev = feed.get_remote_dev(raises=True) if not remote_dev.is_mcready(): continue # check in MusicCast Ready devices only if src_id not in remote_dev.sources_by_id(): continue # no source here remote_zone = feed.get_control_zone() if remote_zone.is_power_on(): # remote zone is already on current_mcid = remote_zone._get_current_input(raises=True).mcid target_mcid = remote_dev.get_input(input_mcid=src_id, raises=True).mcid if current_mcid == target_mcid: # same source! remote_zone_found = True _logger.debug(''.join( ('set_source - using play zone in MusicCast device ', remote_dev.id, '.'))) break else: # the zone is playing another source... Take control and change it remote_zone.set_input(src_id) _logger.debug(''.join( ('set_source - using busy zone in MusicCast device ', remote_dev.id, '.'))) break else: if not remote_zone._amplified: # zone OFF and not powering another location, use it remote_zone.set_power(True) remote_zone.set_input(src_id) remote_zone_found = True _logger.debug(''.join( ('set_source - using a free zone in MusicCast device ', remote_dev.id, '.'))) break if remote_zone_found: _logger.debug(''.join( ('set_source - using zone ', remote_zone.id, ' in remote MusicCast device ', remote_dev.id, '.'))) self.set_input(feed.id) #self._update_usedby_lists(src) return # At this stage there are no usable MusicCast ready sources to use. # We care to find a non-MusicCast source (on a remote device) only to set the right input # on the current zone, but there is nothing to check on the source # (is it on already?, being played for something else?...). We just # switch the input to this feed and hope for the best. # Also, if this device is not MusicCast ready, we do not care anymore, so we can leave. if not self.device.is_mcready(): raise mcx.LogicError(''.join( ('No MusicCast ready source ', src_id, ' found for this non-MusicCast ready zone.'))) for feed in self.device.feeds: remote_dev = feed.get_remote_dev() if remote_dev.musiccast: continue # loop through non-MusicCast devices only if src_id in remote_dev.sources_by_id(): # source found _logger.debug( 'set_source - source found on non MusicCast device') self.set_input( feed.id) # the current device is MusicCast ready _logger.debug(''.join(('set_source - use feed ', feed.id))) return # if we are here, it means we did not find the source in any non MC device either raise mcx.LogicError(''.join( ('No available source ', src_id, ' for this zone.')))