Example #1
0
    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()
Example #2
0
    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
Example #3
0
 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))
Example #4
0
 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))
Example #5
0
    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))
Example #6
0
    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))
Example #7
0
    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))
Example #8
0
    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))
Example #9
0
 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))
Example #10
0
    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
Example #11
0
 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))
Example #12
0
 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))
Example #13
0
    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
Example #14
0
    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))
Example #15
0
    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))
Example #16
0
    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())