示例#1
0
    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
示例#2
0
    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
示例#3
0
    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
示例#4
0
    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
示例#5
0
    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.')))
示例#6
0
    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.')))
示例#7
0
    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.')))
示例#8
0
    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.')))
示例#9
0
    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
示例#10
0
 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
示例#11
0
    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
示例#12
0
    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
示例#13
0
 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
示例#14
0
    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
示例#15
0
    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
示例#16
0
    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
示例#17
0
    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