sonos icon indicating copy to clipboard operation
sonos copied to clipboard

Controller->getSpeakers not updating group

Open 0stone0 opened this issue 3 years ago • 9 comments

First of all, many thanks for this awesome library, and the hotfix adding the new Sonos One SL speakers.


I'm trying to generate a 'state' object of our speakers, where the 'groups' are the leading factor.

I'm using the following code to generate such a 'state' object:

function updateState() {
    $state = [];
    foreach ($this->sonos->getControllers() as $controller) {
        $state[] = (Object) [
            'group'     => $controller->getGroup(),
            'state'     => $controller->getStateName(),
            'room'      => $controller->getRoom(),
            'speakers'  => array_map(function ($speaker) {
                return (Object) [
                    'name' => $speaker->getRoom(),
                    'uuid' => $speaker->getUuid(),
                    'volume' => $speaker->getVolume()
                ];
            }, $controller->getSpeakers())
        ];
    }
    $this->state = $state;
}

This works fine, and when logging this to console every second, I can detect live updates on the volume, state etc.

An example output:

┌─────────────────────────────────────┬──────────┬─────────────────┬───────────────────┐
│ Group                               │ Room     │ State           │ Speakers          │
├─────────────────────────────────────┼──────────┼─────────────────┼───────────────────┤
│ RINCON_38420B52C1280xxxx:3696498370 │ Island 5 │ PLAYING         │ Island 5 [vol=6]  │
├─────────────────────────────────────┼──────────┼─────────────────┼───────────────────┤
│ RINCON_38420B52B54E0xxxx:804389286  │ Island 3 │ PAUSED_PLAYBACK │ Island 3 [vol=9]  │
├─────────────────────────────────────┼──────────┼─────────────────┼───────────────────┤
│ RINCON_38420B52C0120xxxx:2551953165 │ Island 1 │ PLAYING         │ Island 1 [vol=15] │
├─────────────────────────────────────┼──────────┼─────────────────┼───────────────────┤
│ RINCON_38420B52C0420xxxx:3578980849 │ Island 9 │ PLAYING         │ Island 7 [vol=10] │
│                                     │          │                 │ Kitchen [vol=6]   │
│                                     │          │                 │ Island 9 [vol=7]  │
└─────────────────────────────────────┴──────────┴─────────────────┴───────────────────┘

Unfortunately, the 'group' ID won't update when adding/joining speakers together though the Sonos app.

So, in the example above, when adding Island 1 to the Island 9 'group', my updateState() function does not see that change.

Re-starting the script does apply the group change, so my guess is that the groups are not being updated.


Could you please advise if I'm:

  • Not understanding the relation between 'groups' and 'speakers'
  • Not using the correct way of receiving the grouped speakers?
  • Not able to receive 'live' updates on the groups?

0stone0 avatar Apr 21 '22 11:04 0stone0

How are you constructing your $this->sonos object? If you're using caching then changes in the app won't be detected by the library, but if you're just using the default settings that what you're describing should work so there may be a bug

duncan3dc avatar Apr 21 '22 11:04 duncan3dc

I'm using the code from your examples:

$devices = new Discovery();
$devices->setNetworkInterface('en0');

$this->sonos = new Network($devices);

If you want, I can post the complete test script (90 lines).

0stone0 avatar Apr 21 '22 11:04 0stone0

Yes please, then I can reproduce the problem locally and figure out what's going on

duncan3dc avatar Apr 21 '22 11:04 duncan3dc

Please see attached code.
#!/usr/bin/env php
<?php

    require_once __DIR__ . '/vendor/autoload.php';

    use \duncan3dc\Sonos\Network;
    use \duncan3dc\Sonos\Devices\Discovery;
    use \duncan3dc\Sonos\Controller;

    use Symfony\Component\Console\Helper\Table;
    use Symfony\Component\Console\Helper\TableCell;
    use Symfony\Component\Console\Helper\TableSeparator;
    use Symfony\Component\Console\Application;
    use Symfony\Component\Console\Input\InputInterface;
    use Symfony\Component\Console\Output\OutputInterface;

    (new Application('sonos', '1.0.0'))
        ->register('sonos')
        ->setCode(function(InputInterface $input, OutputInterface $output) {
           $app = new GitExample($input, $output);
           $app->run();
        })
        ->getApplication()
        ->setDefaultCommand('sonos', true)
    ->run();

class GitExample {

    function __construct($input, $output) {
        $this->input = $input;
        $this->output = $output;

        $devices = new Discovery();
        $devices->setNetworkInterface('en0');
        $this->sonos = new Network($devices);

        $this->logger = new \Monolog\Logger("sonos");
        $handler = new \Monolog\Handler\StreamHandler("php://stdout", \Monolog\Logger::NOTICE);
        $this->logger->pushHandler($handler);
    }

    function run() {
        while (!0) {
            $this->updateState();
            $this->showState();
            sleep(1);
        }
    }

    function updateState() {
        $state = [];
        foreach ($this->sonos->getControllers() as $controller) {
            $state[] = (Object) [
                'group'     => $controller->getGroup(),
                'state'     => $controller->getStateName(),
                'room'      => $controller->getRoom(),
                'speakers'  => array_map(function ($speaker) {
                    return (Object) [
                        'name' => $speaker->getRoom(),
                        'uuid' => $speaker->getUuid(),
                        'volume' => $speaker->getVolume()
                    ];
                }, $controller->getSpeakers())
            ];
        }
        $this->state = $state;
    }

    function showState() {
        $table = new Table($this->output);
        $table->setStyle('box');
        $table->setHeaders([ 'Group', 'Room', 'State', 'Speakers' ]);

        $rows = [];
        foreach ($this->state as $room) {
            $speakers = array_map(fn ($s) => "{$s->name} [vol={$s->volume}]", $room->speakers);
            $speakers = new TableCell(implode(PHP_EOL, $speakers), [ 'rowspan' => count($speakers) ]);

            $rows[] = [ $room->group, $room->room, $room->state, $speakers ];
            $rows[] = new TableSeparator();
        }

        array_pop($rows);
        $table->setRows($rows);
        $table->render();
    }
}

0stone0 avatar Apr 21 '22 11:04 0stone0

Ah ok, so it is cache, because you've got a long running script the code assumes that only it is in control of the speakers, so it doesn't expect them to change during execution.

You can workaround your specific issue with updateGroup() like so:

function updateState() {
    $state = [];
    foreach ($this->sonos->getControllers() as $controller) {
        $controller->updateGroup();
        $state[] = (Object) [
            'group'     => $controller->getGroup(),
            'state'     => $controller->getStateName(),
            'room'      => $controller->getRoom(),
            'speakers'  => array_map(function ($speaker) {
                return (Object) [
                    'name' => $speaker->getRoom(),
                    'uuid' => $speaker->getUuid(),
                    'volume' => $speaker->getVolume()
                ];
            }, $controller->getSpeakers())
        ];
    }
    $this->state = $state;
}

But I fear this might not be the end of your troubles. If not, please come back on this issue and I'll see if we can implement some kind of "clear all cache except which devices are on the network"

duncan3dc avatar Apr 21 '22 12:04 duncan3dc

Ahh, many thanks!

Calling updateGroup() does indeed 'add' the controller to the correct group, sometimes I'm getting double controllers but that's not a problem:

┌─────────────────────────────────────┬──────────┬─────────┬───────────────────┐
│ Group                               │ Room     │ State   │ Speakers          │
├─────────────────────────────────────┼──────────┼─────────┼───────────────────┤
│ RINCON_38420B52C0120xxxx:2551953165 │ Island 1 │ PLAYING │ Island 1 [vol=15] │
├─────────────────────────────────────┼──────────┼─────────┼───────────────────┤
│ RINCON_38420B52B54E0xxxx:804389287  │ Island 3 │ STOPPED │ Island 3 [vol=9]  │
├─────────────────────────────────────┼──────────┼─────────┼───────────────────┤
│ RINCON_38420B52C0420xxxx:3578980849 │ Island 7 │ PLAYING │ Island 7 [vol=5]  │
│                                     │          │         │ Kitchen [vol=5]   │
│                                     │          │         │ Island 9 [vol=5]  │
├─────────────────────────────────────┼──────────┼─────────┼───────────────────┤
│ RINCON_38420B52C1280xxxx:3696498370 │ Island 5 │ PLAYING │ Island 5 [vol=6]  │
├─────────────────────────────────────┼──────────┼─────────┼───────────────────┤
│ RINCON_38420B52C0420xxxx:3578980849 │ Island 9 │ PLAYING │ Island 7 [vol=5]  │
│                                     │          │         │ Kitchen [vol=5]   │
│                                     │          │         │ Island 9 [vol=5]  │
└─────────────────────────────────────┴──────────┴─────────┴───────────────────┘

Unfortunately, the updateGroup does not remove a device from a group, so when using the Sonos app to disconnect a single speaker from a 'group', it's still in the original group, probably another cachining issue.

Any thoughts on how to tackle this? Clearing the devices before calling the update?

0stone0 avatar Apr 21 '22 12:04 0stone0

For now you'd need to create an entirely new network instance on each loop. But let me take a look this week and see if I can find a simple way to clear off most cache

duncan3dc avatar Apr 21 '22 12:04 duncan3dc

Ahh, creating an entirely new network sound kinda overkill.

Again, many thanks for thinking along.


Since it might help, here's what I'm trying to achieve using your library:

Trying to create a Daemon like script, that will use your library to

  • Communicate with the Sonos speaker
  • Keep a state object of all the speakers/groups

Eventually the script will receive/send Websocket packages to a frontend that can trigger events (next, volume change, grouping speakers)

So the 'state object' needs to detect all kind of changes, the changes your library makes to the state, but also any external changes like the Sonos app.

0stone0 avatar Apr 21 '22 12:04 0stone0

Dear Craig,

Did you happen to look at this?

Looking forward to continuing with the project ;)

Thanks in advance!

0stone0 avatar May 12 '22 13:05 0stone0

Dear Sir,

Excuse me for bumping this issue once more.

I would love to hear if this project is still being maintained, and if this issue is still on the schedule.

Kinds regards, 0stone0

0stone0 avatar Mar 28 '23 16:03 0stone0

Hi @0stone0 it sure is! Sorry for the delay, I'll take a look at this tomorrow

duncan3dc avatar Mar 28 '23 16:03 duncan3dc

Hello again, does this work correctly? If so I can wrap it up into a little convenience method (eg $this->sonos->updateSpeakerGroupings() or something) but it would work the same

function updateState() {
    # Ensure any group changes performed by other applications are picked up
    foreach ($this->sonos->getSpeakers() as $speaker) {
        $speaker->updateGroup();
    }

    $state = [];
    foreach ($this->sonos->getControllers() as $controller) {
        $state[] = (Object) [
            'group'     => $controller->getGroup(),
            'state'     => $controller->getStateName(),
            'room'      => $controller->getRoom(),
            'speakers'  => array_map(function ($speaker) {
                return (Object) [
                    'name' => $speaker->getRoom(),
                    'uuid' => $speaker->getUuid(),
                    'volume' => $speaker->getVolume()
                ];
            }, $controller->getSpeakers())
        ];
    }
    $this->state = $state;
}

duncan3dc avatar Mar 29 '23 09:03 duncan3dc

Hi!

It has been a while since I've played around with this, but at first glance, it seems to work just fine.

Removing / adding speakers to groups are getting update with the added updateGroup.

I will continue developing my applications (probably this weekend), should I experience any other caching related issues, I'll let you know.

Huge thanks.

Kind regards, 0stone0

0stone0 avatar Mar 29 '23 10:03 0stone0

Great news! Thanks for your patience, let me know if you need anything else 👋

duncan3dc avatar Mar 29 '23 15:03 duncan3dc