def set_preset(self, src_mcid=None): '''Set the preset specified in the arguments for the source. Args: source_id (string): the MusicCast 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**. ''' self.device.is_ready(raises=True) self.is_power_on(raises=True) # Resolve the source and check it is the one playing if src_mcid is None: src_mcid = self.device.get_argument('source') src = self.current_input if src_mcid != src.input_mcid: raise mcc.LogicError(''.join( ('Can' 't preset <', src_mcid, '> while device <', self.device.name(), ' is playing input <', src.input_mcid, '>.'))) # Retrieve the number of the preset. try: preset_num = int(self.device.get_argument('preset')) except (KeyError, ValueError): raise mcc.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( self.zone_mcid, args['band'], args['preset_num']) self.device.conn.mcrequest(qualifier, cmdtxt) # Send the command #ctrl_zone.send_reply('OK', ''.join(('preset ', src.input_mcid, # ' to number ', str(preset_num)))) return
def set_playback(self, mc_action, src_mcid=None): '''Triggers the specified play-back action. To be able to play a source, it has to be selected first. Args: action (string): the action to send to the MusicCast device. src_mcid (string): the MusicCast keyword of the source to be played, if supplied, otherwise it is expected to be in the arguments. ''' self.device.is_ready(raises=True) self.is_power_on(raises=True) # Resolve the source and check it is the one playing if src_mcid is None: src_mcid = self.device.get_argument('source') source = self.current_input if src_mcid != source.input_mcid: raise mcc.LogicError(''.join( ('Can not operate source <', src_mcid, '> while device <', self.device.name(), '> is playing <', source.input_mcid, '>.'))) # Send command self.device.conn.mcrequest( source.playinfo_type.type, ''.join( ('setPlayback?playback=', mc_action))) #control_zone.send_reply('OK', ''.join(('playback set to ', action))) 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 source.input_mcid == 'net_radio': args['band'] = '' else: # source.input_mcid not 'net_radio' raise mcc.LogicError(''.join( ('Source ', source.input_mcid, ' does not have presets.'))) if preset_num < 1 or preset_num > self._max_presets: raise mcc.LogicError(''.join( ('Preset ', str(preset_num), ' is out of range.'))) args['preset_num'] = str(preset_num) return args
def get_argument(self, arg): ''' Retrieves argument from arguments dictionary. This method is used by the lambdas to get access to the argument of the message. It is essential that the lambdas are running in the same thread as the one that updated the _msg attribute in the same musiccastDevice instance. Args: arg (string): the name of the argument sought ''' if self._msg is None: raise mcc.LogicError(''.join(('No message to look into.'))) try: value = self._msg.arguments[arg] except KeyError: raise mcc.LogicError(''.join(('No argument <', arg, '> found.'))) return value
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 mcc.LogicError(''.join( ('Source ', source.input_mcid, ' does not have presets.')))
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 mcc.LogicError(''.join( ('Type <', self.type, '> does not have preset info.')))
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 mcc.LogicError(''.join( ('Type <', self.type, '> does not have play message info.')))
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 mcc.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. This method goes hand in hand with the TRANSFORM_ARG dictionary. Args: key (string): internal name of argument. invalue (string): the internal value to be transformed; if provided the transformation is done from this value to the MusicCast value, which is returned. mcvalue (string): 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. ''' try: func = TRANSFORM_ARG[key] # Retrieve the transformation lambdas except KeyError: raise mcc.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 mcc.ConfigError(''.join( ('No valid parameters to transform <', str(key), '> on zone <', self.name(), '> of device <', self.device.name(), '>.'))) try: trsfrm_value = func[trsfrm](self, value) except (TypeError, ValueError) as err: # errors to catch in case of bad format raise mcc.LogicError(''.join( ('Value ', str(value), ' of argument ', str(key), ' seems of the wrong type. Error:\n\t', str(err)))) return trsfrm_value
def _filter_topics(msg): ''' Returns True is topics are valid, False otherwise. ''' if not msg.iscmd: # ignore status messages for now? return False if msg.sender == mcc.APP_NAME: # ignore echoes return False # the following filters could be dealt by subscriptions if not msg.function and not msg.gateway: raise mcc.LogicError('No function or gateway in message.') if msg.gateway and msg.gateway != mcc.APP_NAME: return False if msg.function and msg.function != mcc.APP_FUNCTION: return False return True
def is_ready(self, raises=False): ''' Returns True if the device is ready to be operated. Args: raises (boolean): if True, raises an exception when device is not ready, otherwise it just returns False. Returns: boolean: True if device is ready, False if not and `raises` is False. Raises: LogicError: if the device is not ready and the `raises` argument is True. ''' if self._ready: return True elif raises: raise mcc.LogicError(''.join(('The device ', self.name(), ' is not available.'))) else: return False
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 mcc.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')) self._preset_separate = (preset_type == 'separate') 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 mcc.LogicError('getFeatures item <max_presets> not an int.') # Load the preset_info self._preset_info = None self.update_preset_info() return
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_ready(raises=True) if self._power: return True elif raises: raise mcc.LogicError(''.join( ('The zone ', self.name(), ' of device ', self.device.name(), ' is not turned on.'))) else: return False
def execute_action(self, msg): ''' Executes the action requested in the message This method relies on the ACTIONS dictionary to produce the lambda to execute. Args: msg (:py:class:internalMsg): internal message Raises: LogicError, ConfigError, CommsError: in case of error in executing the lambdas ''' action = msg.action LOG.debug(''.join( ('Execute action <', action, '> on zone <', self.name(), '> of device <', self.device.name(), '>.'))) self._response = '' self._reason = '' try: # retrieve the function to execute for this action func = ACTIONS[action] except KeyError: # the action is not found raise mcc.LogicError(''.join(('Action ', action, ' not found.'))) func(self) # execute the function in the zone return self._response, self._reason
def set_volume(self, vol_up=None): ''' Sets the volume of the zone. Args: vol_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_ready(raises=True) self.is_power_on(raises=True) if vol_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.get_argument('volume')) except (TypeError, ValueError): raise mcc.LogicError('Invalid volume argument') # TODO: check that volume is within range (0-100?) 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.zone_mcid, ''.join(('setVolume?volume=', str(mc_volume)))) else: self.device.conn.mcrequest( self.zone_mcid, ''.join( ('setVolume?volume=', 'up' if vol_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 vol_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(mcvalue=mc_volume) self.status_requested = True self._response = 'OK' self._reason = ''.join(('volume is ', str(volume))) return
def _resolve_zone(self, msg): ''' Finds the zone to operate by resolving the "address" from the topic items. The resolution uses both location and device fields from the topic. The current algorithm is a *strict* one, meaning that if a field is provided, it needs to exist otherwise an exception is thrown. One could imagine a more *tolerant* algorithm if necessary (e.g. if both location and device are provided and the location produces a valid result while the device does not, then the location resolution *wins*). The location defines a zone directly. The device defines only the device (...) so the zone has to be in the arguments otherwise a default is taken (the first zone in the list). This implies that there should always be at least 1 zone in a device and that the first one should be the *main* one if possible. This method should be thread-safe. It uses a re-entrant lock for the devices disctionary. Args: msg (:class:internalMsg): the incoming message to parse. Returns: :class:Zone: a valid Zone object Raises: LogicError, ConfigError. ''' msg.location = None # TODO: implement location processing; ignore location for now self._explicit = msg.gateway or msg.device # defines if to send a reply or not if not msg.location and not msg.device: raise mcc.LogicError('No location or device in message.') # to properly find the right zone and be consistent, we have to lock the dictionary with self._devices_lock: # sets zone_from_location based on the location, None if not found. if msg.location: zone_from_location = None # TODO: implement location processing if msg.device: device = self._get_device_from_id(msg.device, raises=True) # find the zone in the device from the arguments, None if not found zone_id = msg.arguments.get('zone', None) if zone_id is not None: # assume it is the zone mcid. # TODO: implement rename and friendly name zone_from_device = device.get_zone(zone_mcid=zone_id, raises=False) else: zone_from_device = None if msg.location and msg.device: # check consistency. (1) devices found have to be the same if device != zone_from_location.device: raise mcc.LogicError( 'Location and device point to different devices.') # (2) if zone_from_device is defined, the zones need to be the same if zone_from_device is not None: if zone_from_device != zone_from_location: raise mcc.LogicError( 'Location and device point to different zones.') zone_returned = zone_from_location elif msg.device: if zone_from_device is None: zone_from_device = device.zones[ 0] # take the first one by default zone_returned = zone_from_device else: # msg.location is not None zone_returned = zone_from_location return zone_returned