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 get_feature(self, flist): ''' Returns a branch from the getFeatures tree. This method retrieves a branch or leaf from a JSON type object. The argument is a list made of strings and/or pairs of string. For each string, the method expect to find a dictionary as the next branch, and selects the value (which is another branch or leaf) returned by that string as key. For each pair (key, value), it expects to find an array or similar objects, and in that case it searches for the array that contains the right 'value' for that 'key'. This method helps in reading the responses from Yamaha MusicCast API. Args: flist: list representing a leaf or a branch from a JSON type of string Raises: CommsError, ConfigError. ''' branch = self._features if isinstance(flist, basestring): # Python 3: isinstance(arg, str) flist = (flist,) for arg in flist: if isinstance(arg, basestring): try: branch = branch[arg] except KeyError: raise mcx.ConfigError(''.join(('Argument <', str(arg), '> not found in current branch: ', str(branch)))) except TypeError: raise mcx.CommsError(''.join(('The current branch is not a dictionary: ', str(branch)))) else: # assume arg is a pair (key, value) try: key = arg[0] value = arg[1] except (IndexError, TypeError): raise mcx.CommsError(''.join(('Argument <', str(arg), '> should be a pair.'))) found = False for obj in branch: # assume branch is an array try: found = (obj[key] == value) except KeyError: raise mcx.ConfigError(''.join(('Key <', str(key), '> not found in current branch: ', str(branch)))) except TypeError: raise mcx.CommsError(''.join(('The current branch is not a dictionary: ', str(branch)))) if found: break if not found: raise mcx.ConfigError(''.join(('Value <', str(value), '> for key <', str(key), '> not found in array.'))) branch = obj return branch
def get_yxcid(self, raises=False): ''' Returns the Yamaha device id, if it exists.''' if not self.is_mcready(raises): return None try: return self._dev_info['device_id'] except KeyError: if raises: raise mcx.ConfigError('No device_id in getFeatures.') else: return None
def find_infotype(self, play_info_type): ''' Retrieves the info_type instance. Only used (for now) by the event lambdas. ''' try: return self._mcinfotype_d[play_info_type] except KeyError: raise mcx.ConfigError(''.join(('InfoType <', play_info_type, '> not found.')))
def _get_current_input(self, raises=False): ''' Returns the current input in this zone or None Args: raises (boolean): if True, raises an exception if not found ''' if self._input is None and raises: raise mcx.ConfigError('Input unassigned.') return self._input
def get_remote_dev(self, raises=False): ''' Returns the remote Device object. Args: raises (boolean): if True, raises an exception if not found ''' if self._remote_dev is None and raises: raise mcx.ConfigError('Remote device unassigned') return self._remote_dev
def get_control_zone(self, raises=False): ''' Returns the remote Zone object. Args: raises (boolean): if True, raises an exception if not found ''' if self._remote_zone is None and raises: raise mcx.ConfigError('Remote zone unassigned') return self._remote_zone
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 get_zone(self, zone_id=None, zone_mcid=None, raises=False): ''' Returns the :class:`Zone` object from one of its identifications. Either identifications can be provided. The behaviour in case both are provided is determined by the method :meth:`is_zone_id` in :class:`Zone`. 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`` ''' if zone_id is None and zone_mcid is None: if raises: raise mcx.ConfigError('No valid Zone id arguments to get Zone.') else: return None for zone in self.zones: if zone.is_zone_id(zone_id=zone_id, zone_mcid=zone_mcid, raises=False): return zone if raises: raise mcx.ConfigError(''.join(('No Zone found with id ', zone_id if zone_id else zone_mcid, '.'))) else: return None
def find_mczone(self, mcid): ''' Returns the MusicCast zone from its id. Needed by the event processor, it is called by the lambdas. This is redundant with get_zone() but probably faster. Args: mcid (string): MusicCast id of the zone. Normally one of ``main``, ``zone2``, ``zone3``, ``zone4``. See list ``_ZONES``. Raises: ConfigError: if the zone is not found, either because it is not a valid zone or because it does not exist in this device. ''' try: return self._mczone_d[mcid] except KeyError: raise mcx.ConfigError(''.join(('MusicCast zone <', mcid, '> not found in device <', self.id, '>.')))
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 __init__(self, device_data, system): self.system = system self.id = device_data['id'] self._model = device_data.get('model', None) self._protocol = device_data.get('protocol', None) self.gateway = device_data.get('gateway', None) self.musiccast = (self._protocol == 'YEC') if self.musiccast: self._host = device_data['host'] else: self._host = device_data.get('host', None) # Load the feeds, sources and zones from the static data. Zones need to be done at the end. feeds = [] for feed_data in device_data['feeds']: feeds.append(Feed(feed_data, self)) self.feeds = tuple(feeds) sources = [] for source_data in device_data['sources']: sources.append(Source(source_data, self)) self.sources = tuple(sources) self.inputs = self.feeds + self.sources if not self.inputs: raise mcx.ConfigError(''.join(('Device <', self.id, '> has no inputs.'))) self._sources_d = {src.id:src for src in self.sources} self._inputs_d = {src.id:src for src in self.sources} self._inputs_d.update({feed.id:feed for feed in self.feeds}) zones = [] for zone_data in device_data['zones']: zones.append(Zone(zone_data, self)) self.zones = tuple(zones) # MusicCast related attributes if self.musiccast: # this is a MusicCast device self._ready = False self._load_time = 0 # time when load_musiccast was last called self.conn = mcc.musiccastComm(self._host, self.system.listen_port) self._dev_info = None self._features = None # dictionaries to help the event processor, initialised in load_musiccast self._mcinfotype_d = {} self._mczone_d = {} # refresh related attribute self._zone_index = 0 self._zone_num = len(self.zones) return
def init_infotype(self, play_info_type): ''' Returns a new or an existing instance of PlayInfoType. For each type there is only one instance of the corresponding class. Args: play_info_type (string): one of **tuner**, **cd**, or **netusb**. Raises: ConfigError: if the play_info_type is not recognised. ''' if play_info_type in self._mcinfotype_d: return self._mcinfotype_d[play_info_type] if play_info_type == 'tuner': self._mcinfotype_d[play_info_type] = Tuner(self) return self._mcinfotype_d[play_info_type] elif play_info_type == 'cd': self._mcinfotype_d[play_info_type] = CD(self) return self._mcinfotype_d[play_info_type] elif play_info_type == 'netusb': self._mcinfotype_d[play_info_type] = NetUSB(self) return self._mcinfotype_d[play_info_type] else: raise mcx.ConfigError(''.join(('PlayInfoType <', play_info_type, '> does not exist.')))
def get_device(self, device_id=None, yxc_id=None, raises=False): ''' Returns the :class:`Device` object from its id or Yamaha id. Args: device_id (string): the id of the device sought yxc_id (string): the Yamaha hardware id of the device sought raises (boolean): if True, raises an exception instead of returning ``False`` ''' if device_id: try: return self._devices_by_id[device_id] except KeyError: err = ''.join(('Device id <', str(device_id), '> not found.')) elif yxc_id: for dev in self.devices: if yxc_id == dev.get_yxcid(raises=False): return dev err = ''.join(('Yamaha id <', str(yxc_id), '> not found.')) # The code below can be reinstated once the dictionary works #try: return self._devices_by_yxcid[yxc_id] #except KeyError: err = ''.join(('Yamaha id <', str(yxc_id), '> not found.')) else: err = 'No valid argument in get_device()' if raises: raise mcx.ConfigError(err) else: return None
def listen_musiccast(self): ''' Checks if a MusicCast event has arrived and parses it. This method uses the dictionary EVENTS based on all possible fields that a MusicCast can have (see Yamaha doc for more details). This dictionary has only 2 levels and every *node* is either a **dict** or a **callable**. Any *event* object received from a MusicCast device should have a structure which is a subset of the EVENTS one. The algorithm goes through the received *event* structure in parallel of going through the EVENTS one. If there is a key mismatch, the specific key in *event* that cannot find a match in EVENTS is ignored. If there is a key match, the lambda function found as value of that key in EVENTS is called with the value of that same key found in *event* (the *argument*). TODO: check if more than one event could be received in a single call. ''' #event = mcc.get_event() event = self._listener.get_event() if event is None: return # Find device within the event dictionary device_id = event.pop('device_id', None) # read and remove key if device_id is None: raise mcx.CommsError('Event has no device_id. Ignore.') device = self.get_device(yxc_id=device_id, raises=True) # Read event dictionary and call lambda for each key match found flist = [ ] # list of all lambdas to call; the elements are pairs (func, arg) for key1 in event: try: isdict = isinstance(EVENTS[key1], dict) except KeyError: _logger.info(''.join( ('Event has an unknown item <', str(key1), '>. Ignore.'))) continue if isdict: if not isinstance(event[key1], dict): raise mcx.ConfigError( 'Unexpected structure of event. Ignore.') for key2 in event[key1]: try: func = EVENTS[key1][key2] except KeyError: _logger.info(''.join(('Unknown item in event <', str(key2), '>. Ignore.'))) continue if func is not None: flist.append((func, event[key1][key2])) else: func = EVENTS[key1] if func is not None: flist.append((func, event[key1])) # now execute the lambdas while True: try: func, arg = flist.pop(0) except IndexError: break try: func(device, arg) except mcx.AnyError as err: _logger.info(''.join( ('Problem processing event item. Ignore. Error:\n\t', repr(err)))) return