Skip to content

Manage Google Nest speaker groups

Nate edited this page Nov 18, 2020 · 9 revisions

This is an app that was created for managing Groups of Google Nest speakers.

Some of its features:

  • When playing music on a group, change the volume to musicVolume unless the volume has been overridden--in which case, use that.
  • Switch between a "speaking" volume when not playing music and restore the volume to your prior music volume.
  • Adjust the volume based on a day mode and night mode (e.g. once it's sunset, make Assistant talk quieter than during the day).

Hope this helps someone get more familiar with this awesome tool!

configuration.yaml

pyscript:
  apps:
    speaker_group_manager:
      - group: media_player.downstairs_group
        speakers: # the speakers in the group
          media_player.living_room_speaker:
            volumeOverride: None # this is here so we can write a little less code in the script ;)
            musicVolume: 0.25 # default volume for media
            speakingVolume: 0.50 # default volume for the Assistant
          media_player.kitchen_speaker:
            volumeOverride: None
            musicVolume: 0.30
            speakingVolume: 0.50

pyscript/apps/speaker_group_manager.py

registered_triggers = []

def loadApp(app_name, factory):
    if 'apps' not in pyscript.config:
        return
    
    if app_name not in pyscript.config['apps']:
        return

    for app in pyscript.config['apps'][app_name]:
        factory(app)


@time_trigger('startup')
def speakerGroupStartup():
    loadApp('speaker_group_manager', buildSpeakerGroup)


def buildSpeakerGroup(config):
    global registered_triggers
    
    ### Day/Night mode variables ###
    startNightMode = '22:30:00'
    dayMode = f'range(sunrise, {startNightMode})'
    nightMode = f'range({startNightMode}, sunrise)'

    ### config variables ###
    group = config.get('group')
    speakers = config.get('speakers')

    ### conditionals used in the @state_triggers ##
    useMusicVol = f'{group} == "playing"'
    useSpeakingVol = f'{group} != "playing"'

    ### Day mode versions ###

    @time_active(dayMode)
    @state_trigger(useMusicVol)
    def restoreGroupMusicVolume():
        setMusicVolume()

    @time_active(dayMode)
    @state_trigger(useSpeakingVol)
    def restoreGroupSpeakingVolume():
        handleSpeakingVolume()


    ### Night mode versions ###

    @time_active(nightMode)
    @state_trigger(useMusicVol)
    def restoreGroupNightMusicVolume():
        setMusicVolume(nightMode=True)

    @time_active(nightMode)
    @state_trigger(useSpeakingVol)
    def restoreGroupNightSpeakingVolume():
        handleSpeakingVolume(nightMode=True)


    ### Time triggers ###

    # Reset group overrides to defaults at sunrise
    @time_trigger('once(sunrise)')
    def resetGroupOverrides():
        global speakers
        speakers = config.get('speakers')

    # Change group speaker volumes to night mode
    @state_active(f'{group} != "playing"')
    @time_trigger(f'once({startNightMode})')
    def setGroupNightSpeakingVolume():
        setSpeakingVolume(nightMode=True)


    ### Pièce de résistance ###

    # Sets volume overrides for group speakers
    @task_unique(f'monitor_{group}_volume')
    @state_active(f'{group} == "playing"')
    @state_trigger(f'{group}.volume_level')
    def monitorGroupVolume():
        for speaker, volumes in speakers.items():
            volume = state.get(f'{speaker}.volume_level')
            volumes['volumeOverride'] = round(volume, 2)


    ### Helper functions ###

    def setMusicVolume(nightMode=False):
        for speaker, volumes in speakers.items():
            volOverride = volumes['volumeOverride']
            musicVol = volumes['musicVolume'] if (
                volOverride == 'None' or volOverride is None) else volOverride
            volume = musicVol if not nightMode else musicVol - 0.07

            media_player.volume_set(entity_id=speaker, volume_level=volume)

    def setSpeakingVolume(nightMode=False):
        for speaker, volumes in speakers.items():
            speakingVol = volumes['speakingVolume']
            volume = speakingVol if not nightMode else speakingVol - 0.15 # -15 speaking volume when in night mode: arbitrary

            media_player.volume_set(entity_id=speaker, volume_level=volume)

    def handleSpeakingVolume(nightMode=False):
        spotifyDelay = 0.85

        # sometimes the speakers become 'unavailable' while playing
        if state.get(group) != 'unavailable':
            # Spotify changes to 'paused' between songs; wait to alter volume
            if (state.getattr(group)).get('app_name') == 'Spotify':
                waitUntilPlaying = task.wait_until(
                    state_trigger=(f'{group} == "playing"'),
                    timeout=spotifyDelay
                )

                if waitUntilPlaying['trigger_type'] == 'timeout':
                    setSpeakingVolume(nightMode)
            else:
                setSpeakingVolume(nightMode)

    # register to global scope
    registered_triggers.append([
        monitorGroupVolume,
        restoreGroupMusicVolume,
        restoreGroupSpeakingVolume,
        restoreGroupNightMusicVolume,
        restoreGroupNightSpeakingVolume,
        resetGroupOverrides,
        setGroupNightSpeakingVolume
    ])