class Controller(polyinterface.Controller): def __init__(self, polyglot): super(Controller, self).__init__(polyglot) self.name = 'Sonos Controller' # self.poly.onConfig(self.process_config) self.auth_url = 'https://api.sonos.com/login/v3/oauth' self.token_url = 'https://api.sonos.com/login/v3/oauth/access' self.household_url = 'https://api.ws.sonos.com/control/api/v1/households' self.household = {} self.SonosControl = None self.server_data = {} self.cloud = CLOUD self.disco = 0 def start(self): LOGGER.info('Started SonosController NodeServer') self.check_params() if self.get_credentials(): if self.refresh_token(): self.removeNoticesAll() self.discover() else: self.auth_prompt() else: self.auth_prompt() def get_credentials(self): LOGGER.info('---- Environment: ' + self.poly.stage + ' ----') if self.poly.stage == 'test': if 'clientId' in self.poly.init['oauth']['test']: self.server_data['clientId'] = self.poly.init['oauth']['test'][ 'clientId'] else: LOGGER.error('Unable to find Client ID in the init data') return False if 'secret' in self.poly.init['oauth']['test']: self.server_data['clientSecret'] = self.poly.init['oauth'][ 'test']['secret'] else: LOGGER.error('Unable to find Client Secret in the init data') return False if 'redirectUrl' in self.poly.init['oauth']['test']: self.server_data['url'] = self.poly.init['oauth']['test'][ 'redirectUrl'] else: LOGGER.error('Unable to find URL in the init data') return False if self.poly.init['worker']: self.server_data['worker'] = self.poly.init['worker'] else: return False return True elif self.poly.stage == 'prod': if 'clientId' in self.poly.init['oauth']['prod']: self.server_data['clientId'] = self.poly.init['oauth']['prod'][ 'clientId'] else: LOGGER.error('Unable to find Client ID in the init data') return False if 'secret' in self.poly.init['oauth']['test']: self.server_data['clientSecret'] = self.poly.init['oauth'][ 'prod']['secret'] else: LOGGER.error('Unable to find Client Secret in the init data') return False if 'redirectUrl' in self.poly.init['oauth']['test']: self.server_data['url'] = self.poly.init['oauth']['prod'][ 'redirectUrl'] else: LOGGER.error('Unable to find URL in the init data') return False if self.poly.init['worker']: self.server_data['worker'] = self.poly.init['worker'] else: return False return True def auth_prompt(self): _redirect_uri = self.server_data['url'] redirect_uri = requests.utils.quote(_redirect_uri) user_auth_url = self.auth_url + \ '?client_id=' + self.server_data['clientId'] + \ '&response_type=code' \ '&scope=playback-control-all' \ '&state=' + self.server_data['worker'] + \ '&redirect_uri=' + redirect_uri self.addNotice({ 'myNotice': 'Click <a href="' + user_auth_url + '">here</a> to link your Sonos account' }) def oauth(self, oauth): LOGGER.info('OAUTH Received: {}'.format(oauth)) if 'code' in oauth: if self.get_token(oauth['code']): self.remove_notices_all() self.discover() def get_token(self, code): _encode_string = self.server_data['clientId'] + ':' + self.server_data[ 'clientSecret'] byte_encoded = base64.b64encode(_encode_string.encode('UTF-8')) string_encoded = byte_encoded.decode('UTF-8') redirect_uri = self.server_data['url'] + '&state=' + self.server_data[ 'worker'] headers = { 'Authorization': 'Basic ' + string_encoded, 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' } payload = { 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': redirect_uri } r = requests.post(self.token_url, headers=headers, data=payload) resp = r.json() if r.status_code == requests.codes.ok: access_token = resp['access_token'] refresh_token = resp['refresh_token'] expires_in = resp['expires_in'] cust_data = { 'access_token': access_token, 'refresh_token': refresh_token, 'expires_in': expires_in } self.saveCustomData(cust_data) self.SonosControl = SonosControl(access_token) return True else: return False def refresh_token(self): _encode_string = self.server_data['clientId'] + ':' + self.server_data[ 'clientSecret'] byte_encoded = base64.b64encode(_encode_string.encode('UTF-8')) string_encoded = byte_encoded.decode('UTF-8') headers = { 'Authorization': 'Basic ' + string_encoded, 'Content-Type': 'application/x-www-form-urlencoded;charset=utf-8' } if 'refresh_token' in self.polyConfig['customData']: refresh_token = self.polyConfig['customData']['refresh_token'] payload = { 'grant_type': 'refresh_token', 'refresh_token': refresh_token } r = requests.post(self.token_url, headers=headers, data=payload) resp = r.json() if r.status_code == requests.codes.ok: access_token = resp['access_token'] refresh_token = resp['refresh_token'] expires_in = resp['expires_in'] cust_data = { 'access_token': access_token, 'refresh_token': refresh_token, 'expires_in': expires_in } self.saveCustomData(cust_data) self.SonosControl = SonosControl(access_token) self.removeNoticesAll() return True else: return False else: LOGGER.error('Refresh token not in customData') return False def shortPoll(self): if self.disco == 1: try: if self.household is not None: for key in self.household: household = self.household[key] try: sonos_groups = self.SonosControl.get_groups( household) if sonos_groups is not None: for group in sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split( '_')[1][0:-5].lower() playback_state = group['playbackState'] if playback_state == 'PLAYBACK_STATE_PLAYING': playbackstate = 1 elif playback_state == 'PLAYBACK_STATE_TRANSITIONING': playbackstate = 2 elif playback_state == 'PLAYBACK_STATE_PAUSED': playbackstate = 3 elif playback_state == 'PLAYBACK_STATE_IDLE': playbackstate = 4 else: playbackstate = 0 if group_address in self.nodes: self.nodes[group_address].setDriver( 'ST', playbackstate) else: LOGGER.info( "shortPoll:playbackstate: Group does not exist run Re-Discover" ) # List 0=volume, 1=muted, 2=fixed(true/false) group_volume = self.SonosControl.get_group_volume( household, group_id) if group_volume is not None: if group_address in self.nodes: self.nodes[ group_address].setDriver( 'SVOL', group_volume[0]) if group_volume[1]: self.nodes[ group_address].setDriver( 'GV0', 1) else: self.nodes[ group_address].setDriver( 'GV0', 0) else: LOGGER.info( "shortPoll:group_volume: Group does not exist run Re-Discover" ) else: LOGGER.error( "shortPoll:group_volume: None") else: LOGGER.error( "shortPoll:sonos_groups: Sonos Groups is None" ) except KeyError as ex: LOGGER.error( 'shortPoll:sonos_groups: Sonos Groups Error: ' + str(ex)) try: sonos_players = self.SonosControl.get_players( household) if sonos_players is not None: for player in sonos_players: player_id = player['id'] player_address = 'p' + player_id.split( '_')[1][0:-5].lower() player_volume = self.SonosControl.get_player_volume( player_id) # List 0=volume, 1=muted, 2=fixed(true/false) if player_volume is not None: if player_address in self.nodes: self.nodes[ player_address].setDriver( 'SVOL', player_volume[0]) if player_volume[1]: self.nodes[ player_address].setDriver( 'GV0', 1) else: self.nodes[ player_address].setDriver( 'GV0', 0) else: LOGGER.info( "shortPoll: Player node does not exist Run Re-Discover" ) else: LOGGER.error( "shortPoll: SonosControl.get_player_volume is None" ) else: LOGGER.error( "shortPoll: SonosControl.get_players is None" ) except KeyError as ex: LOGGER.error("shortPoll Get Players: " + str(ex)) except: ex = sys.exc_info()[0] LOGGER.error("shortPoll Exception: " + str(ex)) def longPoll(self): self.refresh_token() def query(self): for node in self.nodes: self.nodes[node].reportDrivers() def voice_rss(self): if 'api_key' not in self.polyConfig['customParams']: self.addCustomParam({'api_key': 'none'}) time.sleep(2) if 'language' not in self.polyConfig['customParams']: self.addCustomParam({'language': 'en-us'}) time.sleep(2) if 'codec' not in self.polyConfig['customParams']: self.addCustomParam({'codec': 'mp3'}) time.sleep(2) if 'format' not in self.polyConfig['customParams']: self.addCustomParam({'format': '24khz_16bit_stereo'}) time.sleep(2) def say_tts_params(self): if 'SAY_TTS-1' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-1': 'empty'}) time.sleep(2) if 'SAY_TTS-2' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-2': 'empty'}) time.sleep(2) if 'SAY_TTS-3' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-3': 'empty'}) time.sleep(2) if 'SAY_TTS-4' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-4': 'empty'}) time.sleep(2) if 'SAY_TTS-5' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-5': 'empty'}) time.sleep(2) if 'SAY_TTS-6' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-6': 'empty'}) time.sleep(2) if 'SAY_TTS-7' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-7': 'empty'}) time.sleep(2) if 'SAY_TTS-8' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-8': 'empty'}) time.sleep(2) if 'SAY_TTS-9' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-9': 'empty'}) time.sleep(2) if 'SAY_TTS-10' not in self.polyConfig['customParams']: self.addCustomParam({'SAY_TTS-10': 'empty'}) time.sleep(2) def update_nls(self): """ Updating of Playlists modifies the profile and sends the update """ file_input = 'profile/nls/en_us.txt' if self.household is not None: for key in self.household: household = self.household[key] # Remove PLAY_LIST-NAME, FAVORITE-, SAY_TTS- Entries for line in fileinput.input(file_input, inplace=True, backup='.bak'): if re.match(r'^PLAY_LIST-\d+\s=\s\w+.+', line): pass elif re.match(r'^FAVORITE-\d+\s=\s\w+.+', line): pass elif re.match(r'^SAY_TTS-\d+\s=\s\w+.+', line): pass else: print(line.rstrip()) # Open NLS File in Append Mode nls_file = open(file_input, 'a') # print('Sonos Playlists') sonos_playlists = self.SonosControl.get_playlists(household) for playlist in sonos_playlists: name = sonos_playlists[playlist] nls_file.write('PLAY_LIST-' + str(playlist) + ' = ' + name + '\n') # print('Sonos Favorites') sonos_favorites = self.SonosControl.get_favorites(household) for favorite in sonos_favorites: name = sonos_favorites[favorite] nls_file.write('FAVORITE-' + str(favorite) + ' = ' + name + '\n') # Add new SAY_TTS Entries if 'SAY_TTS-1' in self.polyConfig['customParams']: say_tts1 = self.polyConfig['customParams']['SAY_TTS-1'] nls_file.write('SAY_TTS-1' + ' = ' + say_tts1 + '\n') if 'SAY_TTS-2' in self.polyConfig['customParams']: say_tts2 = self.polyConfig['customParams']['SAY_TTS-2'] nls_file.write('SAY_TTS-2' + ' = ' + say_tts2 + '\n') if 'SAY_TTS-3' in self.polyConfig['customParams']: say_tts3 = self.polyConfig['customParams']['SAY_TTS-3'] nls_file.write('SAY_TTS-3' + ' = ' + say_tts3 + '\n') if 'SAY_TTS-4' in self.polyConfig['customParams']: say_tts4 = self.polyConfig['customParams']['SAY_TTS-4'] nls_file.write('SAY_TTS-4' + ' = ' + say_tts4 + '\n') if 'SAY_TTS-5' in self.polyConfig['customParams']: say_tts5 = self.polyConfig['customParams']['SAY_TTS-5'] nls_file.write('SAY_TTS-5' + ' = ' + say_tts5 + '\n') if 'SAY_TTS-6' in self.polyConfig['customParams']: say_tts6 = self.polyConfig['customParams']['SAY_TTS-6'] nls_file.write('SAY_TTS-6' + ' = ' + say_tts6 + '\n') if 'SAY_TTS-7' in self.polyConfig['customParams']: say_tts7 = self.polyConfig['customParams']['SAY_TTS-7'] nls_file.write('SAY_TTS-7' + ' = ' + say_tts7 + '\n') if 'SAY_TTS-8' in self.polyConfig['customParams']: say_tts8 = self.polyConfig['customParams']['SAY_TTS-8'] nls_file.write('SAY_TTS-8' + ' = ' + say_tts8 + '\n') if 'SAY_TTS-9' in self.polyConfig['customParams']: say_tts9 = self.polyConfig['customParams']['SAY_TTS-9'] nls_file.write('SAY_TTS-9' + ' = ' + say_tts9 + '\n') if 'SAY_TTS-10' in self.polyConfig['customParams']: say_tts10 = self.polyConfig['customParams']['SAY_TTS-10'] nls_file.write('SAY_TTS-10' + ' = ' + say_tts10 + '\n') nls_file.close() def discover(self, *args, **kwargs): if self.SonosControl is not None: self.household = self.SonosControl.get_households() if self.household is not None: for key in self.household: household = self.household[key] try: sonos_groups = self.SonosControl.get_groups(household) if sonos_groups is not None: self.addNode( GroupParentNode(self, 'groups', 'groups', 'Sonos Groups')) time.sleep(2) for group in sonos_groups: coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split( '_')[1][0:-5].lower() group_name = group['name'].split("+")[0] self.addNode( GroupNode(self, 'groups', group_address, group_name, sonos_groups, household)) time.sleep(2) else: LOGGER.error("SonosControl.get_groups is None") except KeyError as ex: LOGGER.error("SonosControl.get_groups Error: " + str(ex)) try: sonos_players = self.SonosControl.get_players(household) if sonos_players is not None: self.addNode( GroupParentNode(self, 'players', 'players', 'Sonos Players')) time.sleep(2) for player in sonos_players: player_id = player['id'] name = player['name'] player_address = 'p' + player_id.split( '_')[1][0:-5].lower() self.addNode( PlayerNode(self, 'players', player_address, name, sonos_players, household)) time.sleep(2) else: LOGGER.error("SonosControl.get_players is None") except KeyError as ex: LOGGER.error("SonosControl.get_players Error: " + str(ex)) self.update_nls() self.poly.installprofile() time.sleep(3) self.disco = 1 self.setDriver('ST', 1, force=True) def delete(self): LOGGER.info('Removing SonosController Nodeserver') def stop(self): LOGGER.debug('NodeServer stopped.') def process_config(self, config): # this seems to get called twice for every change, why? # What does config represent? LOGGER.info("process_config: Enter config={}".format(config)) LOGGER.info("process_config: Exit") def check_params(self): self.voice_rss() time.sleep(2) self.say_tts_params() time.sleep(2) def remove_notice_test(self, command): LOGGER.info('remove_notice_test: notices={}'.format( self.poly.config['notices'])) # Remove all existing notices self.removeNotice('test') def remove_notices_all(self): LOGGER.info('remove_notices_all: notices={}'.format( self.poly.config['notices'])) # Remove all existing notices self.removeNoticesAll() def update_profile(self, command): LOGGER.info('update_profile:') self.update_nls() st = self.poly.installprofile() return st id = 'controller' commands = { # 'QUERY': query, 'DISCOVER': discover, 'UPDATE_PROFILE': update_profile } drivers = [{'driver': 'ST', 'value': 1, 'uom': 2}]
class GroupNode(polyinterface.Node): # def __init__(self, controller, primary, address, name, sonos, sonos_groups, household): def __init__(self, controller, primary, address, name, sonos_groups, household): super(GroupNode, self).__init__(controller, primary, address, name) # self.access_token = None self.SonosControl = None # self.sonos = sonos self.sonos_groups = sonos_groups self.household = household self.shuffle = False def start(self): # print('Starting Group: ' + self.name + ' ------------------') access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: playback_state = group['playbackState'] if playback_state == 'PLAYBACK_STATE_PLAYING': playbackstate = 1 elif playback_state == 'PLAYBACK_STATE_TRANSITIONING': playbackstate = 2 elif playback_state == 'PLAYBACK_STATE_PAUSED': playbackstate = 3 elif playback_state == 'PLAYBACK_STATE_IDLE': playbackstate = 4 else: playbackstate = 0 self.setDriver('ST', playbackstate) volume = self.SonosControl.get_group_volume( self.household, group_id) if volume is not None: # List 0=volume, 1=muted, 2=fixed(true/false) self.setDriver('SVOL', volume[0], force=True) if volume[1] == 'true': self.setDriver('GV0', 1, force=True) else: self.setDriver('GV0', 0, force=True) else: LOGGER.info("Group Volume None for: " + str(group)) # Set Shuffle status off by default self.setDriver('GV1', 0, force=True) def group_volume(self, command): access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) volume = command['value'] for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: _status = self.SonosControl.set_group_volume( self.household, group_id, volume) if _status: self.setDriver('SVOL', volume) else: LOGGER.error('Error group_node.group_volume: ' + str(_status)) def group_mute(self, command): access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: _status = self.SonosControl.set_group_mute(self.household, group_id, mute=True) if _status: self.setDriver('GV0', 1) else: LOGGER.error('Error: ' + str(_status)) def group_unmute(self, command): access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: _status = self.SonosControl.set_group_mute(self.household, group_id, mute=False) if _status: self.setDriver('GV0', 0) else: LOGGER.error('Error: ' + str(_status)) def group_playlist(self, command): access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) playlist = command['value'] for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: _status = self.SonosControl.set_playlist( group_id, playlist, self.shuffle) if _status: self.setDriver('ST', 1) else: LOGGER.error('Error: ' + str(_status)) def group_favorite(self, command): access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) favorite = command['value'] for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: _status = self.SonosControl.set_favorite(group_id, favorite) if _status: self.setDriver('ST', 1) else: LOGGER.error('Error: ' + str(_status)) def group_play(self, command): access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: _status = self.SonosControl.set_play(group_id) if _status: self.setDriver('ST', 1) else: LOGGER.error('Error: ' + str(_status)) def group_pause(self, command): access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: _status = self.SonosControl.set_pause(group_id) if _status: self.setDriver('ST', 3) else: LOGGER.error('Error: ' + str(_status)) def group_skip_to_previous_track(self, command): # print('Previous Track ', command) access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: _status = self.SonosControl.skip_to_previous_track(group_id) if _status: pass else: LOGGER.error('Error: ' + str(_status)) def group_skip_to_next_track(self, command): access_token = self.controller.polyConfig['customData']['access_token'] self.SonosControl = SonosControl(access_token) for group in self.sonos_groups: group_id = group['id'] coordinator_id = group['coordinatorId'] group_address = 'g' + coordinator_id.split('_')[1][0:-5].lower() if group_address == self.address: _status = self.SonosControl.skip_to_next_track(group_id) if _status: pass else: LOGGER.error('Error: ' + str(_status)) def group_shuffle_on(self, command): # print('Shuffle: ', command) self.setDriver('GV1', 1) self.shuffle = True def group_shuffle_off(self, command): # print('Shuffle: ', command) self.setDriver('GV1', 0) self.shuffle = False def query(self): self.reportDrivers() # "Hints See: https://github.com/UniversalDevicesInc/hints" # hint = [1,2,3,4] drivers = [ { 'driver': 'ST', 'value': 0, 'uom': 25 }, { 'driver': 'SVOL', 'value': 0, 'uom': 51 }, { 'driver': 'GV0', 'value': 0, 'uom': 2 }, # Mute/unMute { 'driver': 'PLAYLST', 'value': 0, 'uom': 25 }, { 'driver': 'FAV', 'value': 0, 'uom': 25 }, { 'driver': 'GV1', 'value': 0, 'uom': 2 } # Shuffle ] id = 'GROUP' commands = { 'SVOL': group_volume, 'PLAY': group_play, 'PAUSE': group_pause, 'PREVIOUS': group_skip_to_previous_track, 'NEXT': group_skip_to_next_track, 'MUTE': group_mute, 'UNMUTE': group_unmute, 'PLAYLST': group_playlist, 'FAV': group_favorite, 'SHUFFLEON': group_shuffle_on, 'SHUFFLEOFF': group_shuffle_off }