sonos
sonos copied to clipboard
Controller->getSpeakers not updating group
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?
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
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).
Yes please, then I can reproduce the problem locally and figure out what's going on
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();
}
}
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"
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?
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
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.
Dear Craig,
Did you happen to look at this?
Looking forward to continuing with the project ;)
Thanks in advance!
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
Hi @0stone0 it sure is! Sorry for the delay, I'll take a look at this tomorrow
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;
}
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
Great news! Thanks for your patience, let me know if you need anything else 👋