def get_event(self): ''' Checks the socket for events broadcasted by the MusicCast devices. The 'body' of the event (see below) is in the form: ('{"main":{"power":"on"},"device_id":"00A0DED57E83"}', ('192.168.1.44', 38507)) or: ('{"main":{"volume":88},"zone2":{"volume":0}, "device_id":"00A0DED3FD57"}', ('192.168.1.42', 46514)) ''' # TODO: check max length of the events and if more than one event could arrive at once #if not self._connected: return try: event = select.select([self._socket], [], [], _SOCKET_TIMEOUT) except select.error as err: raise mcx.CommsError(''.join( ('Socket error: ', str(err[1])))) # TODO: throttle this? if event[0]: # there is an event body = event[0][0].recvfrom(1024) _logger.debug(''.join(('Event received: ', str(body)))) try: dict_response = json.loads(body[0]) except ValueError as err: raise mcx.CommsError(''.join( ('The received event is not in JSON format. Error:\n\t', str(err)))) return dict_response return None
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 __init__(self, device): super(NetUSB, self).__init__('netusb', device) self._preset_info = None self._play_time = '0' self._play_message = '' # load the max_preset try: self._max_presets = int(self.device.get_feature(('netusb', 'preset', 'num'))) except ValueError: raise mcx.CommsError('getFeatures item <max_presets> not an int.') # Load the preset_info self._preset_info = None self.update_preset_info() return
def update_preset_info(self): ''' Retrieves the preset_info structure. Info type == **tuner**: the request requires a `band` argument that depends on the features of the device. As the structure returned by the request is a list of objects that always include the band that the preset relates to, we can concatenate all the preset lists. ''' if self._preset_separate: preset_info = [] for band in self._info_bands: response = self.device.conn.mcrequest('tuner', ''.join(('getPresetInfo?band=', band))) try: preset_info.extend(response['preset_info']) except KeyError: raise mcx.CommsError('getPresetInfo did not return a preset_info field.') self._preset_info = preset_info # update attribute only after all worked properly else: response = self.device.conn.mcrequest('tuner', 'getPresetInfo?band=common') try: self._preset_info = response['preset_info'] except KeyError: raise mcx.CommsError('getPresetInfo did not return a preset_info field.') return
def __init__(self, port): self._port = port self._connected = False self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: self._socket.bind(('', self._port)) self._socket.setblocking(0) self._connected = True except socket.error as err: self._socket.close() self._connected = False raise mcx.CommsError(''.join( ('Can\'t open listener socket. Error:\n\t', str(err)))) return
def load_musiccast(self): '''Initialisation of MusicCast related characteristics. This method uses the objects retrieved from previous HTTP requests. It can be called at any time to try again this initialisation. ''' try: for inp in self.device.get_feature(('system', 'input_list')): if inp['id'] == self.mcid: play_info_type = inp['play_info_type'] except KeyError: mcx.CommsError('getFeatures object does not contain the keys '\ '<system>, <input_list>, <id> or <play_info_type>.') self.playinfo_type = self.device.init_infotype(play_info_type) return
def _get_dict_item(self, dico, item): ''' Retrieves the item in the dictionary. This is a safety method in case a structure sent back by MusicCast does not have the item expected. It catches the KeyError exception and changes it into a CommsError one. Args: dico (dict): the dictionary to look into item (string): the key to look for ''' try: return dico[item] except KeyError: raise mcx.CommsError(''.join( ('The dictionary provided by device <', self.device.id, '> does not contain the item <', item, '>')))
def mcrequest(self, qualifier, mc_command): ''' Sends a single HTTP request and returns the response. This method sends the request and read the response step by step in order to catch properly any error in the process. Currently the requests are always with method = 'GET' and version = 'v1'. Args: qualifier (string): the token in the MusicCast syntax representing either a zone or a source, depending on the type of command sent; mc_command (string): the command to send at the end of the request; it has to include any extra argument if there are any. Raises: commsError: in case of any form of Communication Error with the device. Returns: dictionary: the dictionary equivalent of the JSON structure sent back as a reply from the device. ''' conn = httplib.HTTPConnection(self._host, timeout=self._timeout) _logger.debug(''.join( ('Sending to address <', self._host, '> the request: ', '/'.join( ('/YamahaExtendedControl/v1', qualifier, mc_command))))) try: conn.request(method='GET', url='/'.join(('/YamahaExtendedControl/v1', qualifier, mc_command)), headers=self._headers) except httplib.HTTPException as err: conn.close() raise mcx.CommsError(''.join( ('Can\'t send request. Error:\n\t', str(err)))) except socket.timeout: conn.close() raise mcx.CommsError('Can\'t send request. Connection timed-out.') except socket.error as err: conn.close() raise mcx.CommsError(''.join( ('Can\'t send request. Socket error:\n\t', str(err)))) # insert a delay here? try: response = conn.getresponse() except httplib.HTTPException as err: conn.close() raise mcx.CommsError(''.join( ('Can\'t get response. Error:\n\t', str(err)))) except socket.timeout: conn.close() raise mcx.CommsError('Can\'t get response. Connection timed-out.') except socket.error as err: conn.close() raise mcx.CommsError(''.join( ('Can\'t get response. Socket error:\n\t', str(err)))) if response.status != 200: conn.close() raise mcx.CommsError(''.join(('HTTP response status not OK.'\ '\n\tStatus: ', httplib.responses[response.status], '\n\tReason: ', response.reason))) try: dict_response = json.loads(response.read()) except ValueError as err: conn.close() raise mcx.CommsError(''.join(('The response from the device is not'\ ' in JSON format. Error:\n\t', str(err)))) if dict_response['response_code'] != 0: conn.close() raise mcx.CommsError(''.join(('The response code from the'\ ' MusicCast device is not OK. Actual code:\n\t', str(dict_response['response_code'])))) _logger.debug('Request answered successfully.') conn.close() self.request_time = time.time() return dict_response
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