def set_group(self, id, name=None, source_id=None, zones=None, mute=None, vol_delta=None): """Configures an existing group parameters will be used to configure each sone in the group's zones all parameters besides the group id, @id, are optional Args: id: group id (a guid) name: group name source_id: group source zones: zones that belong to the group mute: group mute setting (muted=True) vol_delta: volume adjustment to apply to each zone [-79,79] Returns: 'None' on success, otherwise error (dict) """ _, g = self.get_group(id) if g is None: return utils.error( 'set group failed, group {} not found'.format(id)) if type(zones) is str: try: zones = eval(zones) except Exception as e: return utils.error( 'failed to configure group, error parsing zones: {}'. format(e)) try: name, _ = utils.updated_val(name, g['name']) zones, _ = utils.updated_val(zones, g['zones']) vol_delta, vol_updated = utils.updated_val(vol_delta, g['vol_delta']) if vol_updated: vol_change = vol_delta - g['vol_delta'] else: vol_change = 0 except Exception as e: return utils.error( 'failed to configure group, error getting current state: {}'. format(e)) g['name'] = name g['zones'] = zones for z in [self.status['zones'][zone] for zone in zones]: if vol_change != 0: # TODO: make this use volume delta adjustment, for now its a fixed group volume vol = vol_delta # vol = z['vol'] + vol_change else: vol = None self.set_zone(z['id'], None, source_id, mute, vol) g['vol_delta'] = vol_delta # update the group stats self._update_groups()
def create_group(self, name, zones): """Creates a new group with a list of zones Refer to the returned system state to obtain the id for the newly created group """ # verify new group's name is unique names = [g['name'] for g in self.status['groups']] if name in names: return utils.error( 'create group failed: {} already exists'.format(name)) if type(zones) is str: try: zones = eval(zones) except Exception as e: return utils.error( 'failed to configure group, error parsing zones: {}'. format(e)) # get the new groug's id id = self._new_group_id() # add the new group group = {'id': id, 'name': name, 'zones': zones, 'vol_delta': 0} self.status['groups'].append(group) # update the group stats and populate uninitialized fields of the group self._update_groups() return group
def delete_preset(self, id): """Deletes an existing preset""" try: i, _ = utils.find(self.status['presets'], id) if i is not None: del self.status['presets'][ i] # delete the cached preset state just in case else: return utils.error( 'delete preset failed: {} does not exist'.format(id)) except KeyError: return utils.error( 'delete preset failed: {} does not exist'.format(id))
def create_stream(self, **kwargs): try: # Make a new stream and add it to streams s = streams.build_stream(args=kwargs, mock=self._mock_streams) id = self._new_stream_id() self.streams[id] = s # Get the stream as a dictionary (we use get_state() to convert it from its stream type into a dict) for s in self.get_state()['streams']: if s['id'] == id: return s return utils.error('create stream failed: no stream created') except Exception as e: return utils.error('create stream failed: {}'.format(e))
def set_preset(self, id, preset_changes): i, preset = utils.find(self.status['presets'], id) configurable_fields = ['name', 'commands', 'state'] if i is None: return utils.error('Unable to find preset to redefine') try: # TODO: validate preset for f in configurable_fields: if f in preset_changes: preset[f] = preset_changes[f] except Exception as e: return utils.error('Unable to reconfigure preset {}: {}'.format( id, e))
def set_stream(self, id, **kwargs): if int(id) not in self.streams: return utils.error('Stream id {} does not exist'.format(id)) try: stream = self.streams[int(id)] except Exception as e: return utils.error('Unable to get stream {}: {}'.format(id, e)) try: stream.reconfig(**kwargs) except Exception as e: return utils.error('Unable to reconfigure stream {}: {}'.format( id, e))
def set_stream(self, id, **kwargs): """Sets play/pause on a specific pandora source """ if int(id) not in self.streams: return utils.error('Stream id {} does not exist'.format(id)) try: stream = self.streams[int(id)] except Exception as e: return utils.error('Unable to get stream {}: {}'.format(id, e)) try: stream.reconfig(**kwargs) except Exception as e: return utils.error('Unable to reconfigure stream {}: {}'.format( id, e))
def exec_stream_command(self, id, cmd): """Sets play/pause on a specific pandora source """ # TODO: this needs to be handled inside the stream itself, each stream can have a set of commands available if int(id) not in self.streams: return utils.error('Stream id {} does not exist'.format(id)) try: stream = self.streams[int(id)] except Exception as e: return utils.error('Unable to get stream {}: {}'.format(id, e)) try: if cmd is None: pass elif cmd == 'play': print('playing') stream.state = 'playing' stream.ctrl.play() elif cmd == 'pause': print('paused') stream.state = 'paused' stream.ctrl.pause() elif cmd == 'stop': stream.state = "stopped" stream.ctrl.stop() elif cmd == 'next': print('next') stream.ctrl.next() elif cmd == 'love': stream.ctrl.love() elif cmd == 'ban': stream.ctrl.ban() elif cmd == 'shelve': stream.ctrl.shelve() elif 'station' in cmd: station_id = int(cmd.replace('station=', '')) if station_id is not None: stream.ctrl.station(station_id) else: return utils.error( 'station=<int> expected where <int> is a valid integer, ie. station=23432423, received "{}"' .format(cmd)) else: return utils.error('Command "{}" not recognized.'.format(cmd)) except Exception as e: return utils.error( 'Failed to execute stream command: {}: {}'.format(cmd, e))
def delete_group(self, id): """Deletes an existing group""" try: i, _ = self.get_group(id) if i is not None: del self.status['groups'][i] except KeyError: return utils.error( 'delete group failed: {} does not exist'.format(id))
def set_stream(self, id, name=None, station_id=None, cmd=None): """Sets play/pause on a specific pandora source """ if int(id) not in self.streams: return utils.error('Stream id {} does not exist!'.format(id)) # try: # strm = self.status['streams'][id] # name, _ = utils.updated_val(name, strm['name']) # except: # return utils.error('ERROR!') # TODO: this needs to be handled inside the stream itself, each stream can have a set of commands available try: if cmd == 'play': print('playing') self.streams[id].state = 'playing' self.streams[id].ctrl.play() elif cmd == 'pause': print('paused') self.streams[id].state = 'paused' self.streams[id].ctrl.pause() elif cmd == 'stop': self.streams[id].state = "stopped" self.streams[id].ctrl.stop() elif cmd == 'next': print('next') self.streams[id].ctrl.next() elif cmd == 'love': self.streams[id].ctrl.love() elif cmd == 'ban': self.streams[id].ctrl.ban() elif cmd == 'shelve': self.streams[id].ctrl.shelve() elif cmd == 'station': if station_id is not None: self.streams[id].ctrl.station(station_id) else: return utils.error( 'Station_ID required. Please try again.') else: print('Command "{}" not recognized.'.format(cmd)) except Exception as e: print('error setting stream: {}'.format(e)) pass # TODO: actually report error
def delete_stream(self, id): """Deletes an existing stream""" try: del self.streams[id] i, _ = utils.find(self.status['streams'], id) if i is not None: del self.status['streams'][ i] # delete the cached stream state just in case except KeyError: return utils.error( 'delete stream failed: {} does not exist'.format(id))
def create_preset(self, preset): try: # Make a new preset and add it to presets # TODO: validate preset id = self._new_preset_id() preset['id'] = id preset[ 'last_used'] = None # indicates this preset has never been used self.status['presets'].append(preset) return preset except Exception as e: return utils.error('create preset failed: {}'.format(e))
def get_stations(self, id, stream_index=None): """Gets a pandora stream's station list""" # TODO: this should be moved to be a command of the Pandora stream interface if id not in self.streams: return utils.error('Stream id {} does not exist!'.format(id)) # TODO: move the rest of this into streams if stream_index is not None: root = '/home/pi/config/srcs/{}/'.format(stream_index) else: root = '/home/pi/' stat_dir = root + '.config/pianobar/stationList' try: with open(stat_dir, 'r') as file: d = {} for line in file.readlines(): line = line.strip() if line: data = line.split(':') d[data[0]] = data[1] return (d) except Exception as e: # TODO: throw useful exceptions to next level pass
def set_zone(self, id, name=None, source_id=None, mute=None, vol=None, disabled=None, force_update=False): """Configures a zone Args: id (int): any valid zone [0,p*6-1] (6 zones per preamp) name(str): friendly name for the zone, ie "bathroom" or "kitchen 1" source_id (int): source to connect to [0,4] mute (bool): mute the zone regardless of set volume vol (int): attenuation [-79,0] 0 is max volume, -79 is min volume disabled (bool): disable zone, for when the zone is not connected to any speakers and not in use force_update: bool, update source even if no changes have been made (for hw startup) Returns: 'None' on success, otherwise error (dict) """ idx = None for i, s in enumerate(self.status['zones']): if s['id'] == int(id): idx = i if idx is not None: try: z = self.status['zones'][idx] # TODO: use updated? value name, _ = utils.updated_val(name, z['name']) source_id, update_source_id = utils.updated_val( source_id, z['source_id']) mute, update_mutes = utils.updated_val(mute, z['mute']) vol, update_vol = utils.updated_val(vol, z['vol']) disabled, _ = utils.updated_val(disabled, z['disabled']) except Exception as e: return utils.error( 'failed to set zone, error getting current state: {}'. format(e)) try: sid = utils.parse_int(source_id, [0, 1, 2, 3, 4]) vol = utils.parse_int( vol, range(-79, 79) ) # hold additional state for group delta volume adjustments, output volume will be saturated to 0dB zones = self.status['zones'] # update non hw state z['name'] = name z['disabled'] = disabled # TODO: figure out an order of operations here, like does mute need to be done before changing sources? if update_source_id or force_update: zone_sources = [zone['source_id'] for zone in zones] zone_sources[idx] = sid if self._rt.update_zone_sources(idx, zone_sources): z['source_id'] = sid else: return utils.error( 'set zone failed: unable to update zone source') if update_mutes or force_update: mutes = [zone['mute'] for zone in zones] mutes[idx] = mute if self._rt.update_zone_mutes(idx, mutes): z['mute'] = mute else: return utils.error( 'set zone failed: unable to update zone mute') if update_vol or force_update: real_vol = utils.clamp(vol, -79, 0) if self._rt.update_zone_vol(idx, real_vol): z['vol'] = vol else: return utils.error( 'set zone failed: unable to update zone volume') # update the group stats (individual zone volumes, sources, and mute configuration can effect a group) self._update_groups() return None except Exception as e: return utils.error('set zone: ' + str(e)) else: return utils.error('set zone: index {} out of bounds'.format(idx))
def set_source(self, id, name=None, input=None, force_update=False): """Modifes the configuration of one of the 4 system sources Args: id (int): source id [0,3] name (str): user friendly source name, ie. "cd player" or "stream 1" input: method of audio input ('local', 'stream=ID') force_update: bool, update source even if no changes have been made (for hw startup) Returns: 'None' on success, otherwise error (dict) """ idx = None for i, s in enumerate(self.status['sources']): if s['id'] == id: idx = i if idx is not None: try: src = self.status['sources'][idx] name, _ = utils.updated_val(name, src['name']) input, input_updated = utils.updated_val(input, src['input']) except Exception as e: return utils.error( 'failed to set source, error getting current state: {}'. format(e)) try: # update the name src['name'] = str(name) if input_updated or force_update: # shutdown old stream old_stream = self.get_stream(src['input']) if old_stream: old_stream.disconnect() # start new stream stream = self.get_stream(input) if stream: # update the streams last connected source to have no input, since we have stolen its input if stream.src is not None and stream.src != idx: other_src = self.status['sources'][stream.src] print('stealing {} from source {}'.format( stream.name, other_src['name'])) other_src['input'] = '' else: print('stream.src={} idx={}'.format( stream.src, idx)) stream.disconnect() stream.connect(idx) rt_needs_update = self._is_digital( input) != self._is_digital(src['input']) if rt_needs_update or force_update: # get the current underlying type of each of the sources, for configuration of the runtime src_cfg = [ self._is_digital( self.status['sources'][s]['input']) for s in range(4) ] # update this source src_cfg[idx] = self._is_digital(input) if self._rt.update_sources(src_cfg): # update the status src['input'] = input return None else: return utils.error('failed to set source') else: src['input'] = input except Exception as e: return utils.error('failed to set source: ' + str(e)) else: return utils.error( 'failed to set source: index {} out of bounds'.format(idx))
def load_preset(self, id): """ To avoid any issues with audio coming out of the wrong speakers, we will need to carefully load a preset configuration. Below is an idea of how a preset configuration could be loaded to avoid any weirdness. We are also considering adding a "Last config" preset that allows us to easily revert the configuration changes. 1. Grab system modification mutex to avoid accidental changes (requests during this time return some error) 2. Save current configuration as "Last config" preset 3. Mute any effected zones 4. Execute changes to source, zone, group each in increasing order 5. Unmute effected zones that were not muted 6. Execute any stream commands 7. Release system mutex, future requests are successful after this """ # Get the preset to load LAST_PRESET = 9999 i, preset = utils.find(self.status['presets'], id) if i is None: return utils.error( 'load preset failed: {} does not exist'.format(id)) # TODO: acquire lock (all api methods that change configuration will need this) # update last config preset for restore capabilities (creating if missing) # TODO: "last config" does not currently support restoring streaming state, how would that work? (maybe we could just support play/pause state?) lp, _ = utils.find(self.status['presets'], LAST_PRESET) st = self.status last_config = { 'id': 9999, 'name': 'Restore last config', 'last_used': None, # this need to be in javascript time format 'state': { 'sources': deepcopy(st['sources']), 'zones': deepcopy(st['zones']), 'groups': deepcopy(st['groups']) } } if lp is None: self.status['presets'].append(last_config) else: self.status['presets'][lp] = last_config # keep track of the zones muted by the preset configuration z_muted = set() # determine which zones will be effected and mute them # we do this just in case there is intermediate state that causes audio issues. TODO: test with and without this feature (it adds a lot of complexity to presets, lets make sure its worth it) z_effected = self._effected_zones(id) z_temp_muted = [ zid for zid in z_effected if not self.status['zones'][zid]['mute'] ] for zid in z_temp_muted: self.set_zone(zid, mute=True) ps = preset['state'] # execute changes source by source in increasing order for src in ps.get('sources', []): if 'id' in src: self.set_source(**src) else: pass # TODO: support some id-less source concept that allows dynamic source allocation # execute changes group by group in increasing order for g in utils.with_id(ps.get('groups')): _, g_to_update = utils.find(self.status['groups'], g['id']) if g_to_update is None: return utils.error('group {} does not exist'.format(g['id'])) self.set_group(**g) if 'mute' in g: # use the updated group's zones just in case the group's zones were just changed _, g_updated = utils.find(self.status['groups'], g['id']) zones_changed = g_updated['zones'] if g['mute']: # keep track of the muted zones z_muted.update(zones_changed) else: # handle odd mute thrashing case where zone was muted by one group then unmuted by another z_muted.difference_update() # execute change zone by zone in increasing order for z in utils.with_id(ps.get('zones')): self.set_zone(**z) if 'mute' in z: if z['mute']: z_muted.add(z['id']) elif z['id'] in z_muted: z_muted.remove(z['id']) # unmute effected zones that were not muted by the preset configuration z_to_unmute = set(z_temp_muted).difference(z_muted) for zid in z_to_unmute: self.set_zone(id=zid, mute=False) # TODO: execute stream commands preset['last_used'] = int(time.time())