Ejemplo n.º 1
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.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
Ejemplo n.º 2
0
    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
Ejemplo n.º 3
0
 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
Ejemplo n.º 4
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 mcx.LogicError(''.join(('Type <', self.type, '> does not have preset info.')))
Ejemplo n.º 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 mcx.LogicError(''.join(('Source ', source.mcid, ' does not have presets.')))
Ejemplo n.º 6
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 mcx.LogicError(''.join(('Type <', self.type, '> does not have play message info.')))
Ejemplo n.º 7
0
 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
Ejemplo n.º 8
0
    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.')))
Ejemplo n.º 9
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 mcx.LogicError(''.join(('Type <', self.type, '> does not have play time info.')))
Ejemplo n.º 10
0
    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
Ejemplo n.º 11
0
    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
Ejemplo n.º 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 mcx.LogicError(''.join(('Preset ', str(preset_num), ' is out of range.')))
        args['preset_num'] = str(preset_num)
        return args
Ejemplo n.º 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'))
     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
Ejemplo n.º 14
0
    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
Ejemplo n.º 15
0
    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
Ejemplo n.º 16
0
    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
Ejemplo n.º 17
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_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
Ejemplo n.º 18
0
    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
Ejemplo n.º 19
0
    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
Ejemplo n.º 20
0
    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.')))