Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
root = true

[*.cs]
indent_size = 4
trim_trailing_whitespace = true
max_line_length = off
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ var sonosDevice = await service.GetDeviceAsync("192.168.1.100");

var httpResponseMessage = await service.RebootAsync("192.168.1.100");
```

### Sonos API Documentation

https://svrooij.io/sonos-api-docs/
3 changes: 2 additions & 1 deletion src/ByteDev.Sonos.Console/SonosOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ private SonosController CreateSonosController(string ipAddress)
{
return new SonosController(new AvTransportService(ipAddress),
new RenderingControlService(ipAddress),
new ContentDirectoryService(ipAddress));
new ContentDirectoryService(ipAddress),
new ZoneGroupTopologyService(ipAddress));
}

private static void Print(string message)
Expand Down
497 changes: 255 additions & 242 deletions src/ByteDev.Sonos.Ui/MainForm.Designer.cs

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions src/ByteDev.Sonos.Ui/MainForm.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
Expand Down Expand Up @@ -233,6 +234,24 @@ private async void rebootButton_Click(object sender, EventArgs e)
}
}

private async void getGroupsButton_Click(object sender, EventArgs e)
{
var controller = _sonosControllerFactory.Create(ipAddressTextBox.Text);
var zoneGroupState = await controller.GetGroupsAsync();

ResetOutput();

foreach (var zoneGroup in zoneGroupState.ZoneGroups.Items)
{
var name = zoneGroup.ZoneGroupMembers.FirstOrDefault().ZoneName;
var members = string.Join(", ", zoneGroup.ZoneGroupMembers.Select(m => m.UUID));

PrintOutput($"{name} (Coordinator is {zoneGroup.Id}) - {zoneGroup.ZoneGroupMembers.Length} members [{members}]");
}

PrintOutput($"{zoneGroupState.ZoneGroups.Items.Length} groups.");
}

private void PrintOutput(SonosDevice device)
{
ResetOutput();
Expand Down
16 changes: 16 additions & 0 deletions src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroup.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Xml.Serialization;

namespace ByteDev.Sonos.Upnp.Services.Models
{
public class ZoneGroup
{
[XmlElement("ZoneGroupMember")]
public ZoneGroupMember[] ZoneGroupMembers { get; set; }

[XmlAttribute]
public string Coordinator { get; set; }

[XmlAttribute("ID")]
public string Id { get; set; }
}
}
97 changes: 97 additions & 0 deletions src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupMember.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using System.Xml.Serialization;

namespace ByteDev.Sonos.Upnp.Services.Models
{
public class ZoneGroupMember
{
[XmlAttribute]
public string UUID { get; set; }

[XmlAttribute]
public string Location { get; set; }

[XmlAttribute]
public string ZoneName { get; set; }

[XmlAttribute]
public string Icon { get; set; }

[XmlAttribute]
public byte Configuration { get; set; }

[XmlAttribute]
public byte Invisible { get; set; }

[XmlAttribute]
public string SoftwareVersion { get; set; }

[XmlAttribute]
public byte SWGen { get; set; }

[XmlAttribute]
public string MinCompatibleVersion { get; set; }

[XmlAttribute]
public string LegacyCompatibleVersion { get; set; }

[XmlAttribute]
public string ChannelMapSet { get; set; }

[XmlAttribute]
public byte BootSeq { get; set; }

[XmlAttribute]
public byte TVConfigurationError { get; set; }

[XmlAttribute]
public byte HdmiCecAvailable { get; set; }

[XmlAttribute]
public byte WirelessMode { get; set; }

[XmlAttribute]
public byte WirelessLeafOnly { get; set; }

[XmlAttribute]
public ushort ChannelFreq { get; set; }

[XmlAttribute]
public byte BehindWifiExtender { get; set; }

[XmlAttribute]
public byte WifiEnabled { get; set; }

[XmlAttribute]
public byte Orientation { get; set; }

[XmlAttribute]
public byte RoomCalibrationState { get; set; }

[XmlAttribute]
public byte SecureRegState { get; set; }

[XmlAttribute]
public byte VoiceConfigState { get; set; }

[XmlAttribute]
public byte MicEnabled { get; set; }

[XmlAttribute]
public byte AirPlayEnabled { get; set; }

[XmlAttribute]
public byte IdleState { get; set; }

[XmlAttribute]
public string MoreInfo { get; set; }

[XmlAttribute]
public ushort SSLPort { get; set; }

[XmlAttribute]
public ushort HHSSLPort { get; set; }

[XmlAttribute]
public string VirtualLineInSource { get; set; }
}
}
9 changes: 9 additions & 0 deletions src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace ByteDev.Sonos.Upnp.Services.Models
{
public class ZoneGroupState
{
public ZoneGroups ZoneGroups { get; set; }

public object VanishedDevices { get; set; }
}
}
10 changes: 10 additions & 0 deletions src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupStateZoneGroups.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using System.Xml.Serialization;

namespace ByteDev.Sonos.Upnp.Services.Models
{
public class ZoneGroups
{
[XmlElement("ZoneGroup")]
public ZoneGroup[] Items{ get; set; }
}
}
33 changes: 33 additions & 0 deletions src/ByteDev.Sonos.Upnp/Services/ZoneGroupTopologyService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.IO;
using System.Threading.Tasks;
using System.Xml.Serialization;
using ByteDev.Sonos.Upnp.Proxy;
using ByteDev.Sonos.Upnp.Services.Models;

namespace ByteDev.Sonos.Upnp.Services
{
public class ZoneGroupTopologyService
{
private readonly IUpnpClient _upnpClient;
private static readonly XmlSerializer XmlSerializer = new XmlSerializer(typeof(ZoneGroupState));

private const string ControlUrl = "/ZoneGroupTopology/Control";
private const string ActionNamespace = "urn:schemas-upnp-org:service:ZoneGroupTopology:1";

public ZoneGroupTopologyService(string ipAddress)
{
var upnpUri = new SonosUri(ipAddress, ControlUrl);
_upnpClient = new UpnpClient(upnpUri.ToUri(), ActionNamespace);
}

public async Task<ZoneGroupState> GetZoneGroupStateAsync()
{
var xmlResponse = await _upnpClient.InvokeFuncAsync<string>("GetZoneGroupState");
using (var responseStream = new StringReader(xmlResponse))
{
var zoneGroupState = (ZoneGroupState) XmlSerializer.Deserialize(responseStream);
return zoneGroupState;
}
}
}
}
16 changes: 14 additions & 2 deletions src/ByteDev.Sonos/SonosController.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading;

using System.Threading;
using System.Threading.Tasks;
using ByteDev.Sonos.Models;
using ByteDev.Sonos.Upnp.Services;
Expand All @@ -11,14 +12,16 @@ public class SonosController
private readonly IAvTransportService _avTransportService;
private readonly IRenderingControlService _renderingControlService;
private readonly IContentDirectoryService _contentDirectoryService;
private readonly ZoneGroupTopologyService _zoneGroupTopologyService;

public SonosController(IAvTransportService avTransportService,
IRenderingControlService renderingControlService,
IContentDirectoryService contentDirectoryService)
IContentDirectoryService contentDirectoryService, ZoneGroupTopologyService zoneGroupTopologyService)
{
_avTransportService = avTransportService;
_renderingControlService = renderingControlService;
_contentDirectoryService = contentDirectoryService;
_zoneGroupTopologyService = zoneGroupTopologyService;
}

#region Speaker
Expand Down Expand Up @@ -177,5 +180,14 @@ public async Task SeekTrackAsync(SonosTrackNumber trackNumber)
}

#endregion

#region Groups

public async Task<ZoneGroupState> GetGroupsAsync()
{
return await _zoneGroupTopologyService.GetZoneGroupStateAsync();
}

#endregion
}
}
3 changes: 2 additions & 1 deletion src/ByteDev.Sonos/SonosControllerFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ public SonosController Create(string ipAddress)
{
return new SonosController(new AvTransportService(ipAddress),
new RenderingControlService(ipAddress),
new ContentDirectoryService(ipAddress));
new ContentDirectoryService(ipAddress),
new ZoneGroupTopologyService(ipAddress));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public class AvTransportServiceTest
private SonosController _sonosController;
private RenderingControlService _renderingControlService;
private ContentDirectoryService _contentDirectoryService;
private ZoneGroupTopologyService _zoneGroupTopologyService;

[SetUp]
public void SetUp()
Expand All @@ -23,10 +24,12 @@ public void SetUp()

_renderingControlService = new RenderingControlService(TestSpeaker.IpAddress);
_contentDirectoryService = new ContentDirectoryService(TestSpeaker.IpAddress);
_zoneGroupTopologyService = new ZoneGroupTopologyService(TestSpeaker.IpAddress);

_sonosController = new SonosController(_sut,
_renderingControlService,
_contentDirectoryService);
_contentDirectoryService,
_zoneGroupTopologyService);
}

private async Task AssertIsPlayingAsync()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using System.Threading.Tasks;
using ByteDev.Sonos.Testing;
using ByteDev.Sonos.Upnp.Services;
using NUnit.Framework;

namespace ByteDev.Sonos.Upnp.IntTests.Services
{
[TestFixture]
public class ZoneGroupTopologyServiceTest
{
private ZoneGroupTopologyService _zoneGroupTopologyService;

[SetUp]
public void SetUp()
{
_zoneGroupTopologyService = new ZoneGroupTopologyService(TestSpeaker.IpAddress);
}

[Test]
public async Task WhenSpeakerExists_ThenReturnVolume()
{
var zoneGroupState = await _zoneGroupTopologyService.GetZoneGroupStateAsync().ConfigureAwait(false);

Assert.That(zoneGroupState, Is.Not.Null);
Assert.That(zoneGroupState.ZoneGroups.Items.Length, Is.GreaterThan(0));
}
}
}