From 1ee1c4db9775f471a5daf06908a6fdf942bc717e Mon Sep 17 00:00:00 2001 From: "A. Babilone" Date: Sun, 3 Apr 2022 08:49:07 +0200 Subject: [PATCH] Add support for ZoneGroupTopology.GetZoneGroupState --- .editorconfig | 6 + README.md | 4 + src/ByteDev.Sonos.Console/SonosOperations.cs | 3 +- src/ByteDev.Sonos.Ui/MainForm.Designer.cs | 497 +++++++++--------- src/ByteDev.Sonos.Ui/MainForm.cs | 19 + .../Services/Models/ZoneGroup.cs | 16 + .../Services/Models/ZoneGroupMember.cs | 97 ++++ .../Services/Models/ZoneGroupState.cs | 9 + .../Models/ZoneGroupStateZoneGroups.cs | 10 + .../Services/ZoneGroupTopologyService.cs | 33 ++ src/ByteDev.Sonos/SonosController.cs | 16 +- src/ByteDev.Sonos/SonosControllerFactory.cs | 3 +- .../Services/AvTransportServiceTest.cs | 5 +- .../Services/ZoneGroupTopologyServiceTest.cs | 28 + 14 files changed, 499 insertions(+), 247 deletions(-) create mode 100644 .editorconfig create mode 100644 src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroup.cs create mode 100644 src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupMember.cs create mode 100644 src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupState.cs create mode 100644 src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupStateZoneGroups.cs create mode 100644 src/ByteDev.Sonos.Upnp/Services/ZoneGroupTopologyService.cs create mode 100644 tests/ByteDev.Sonos.Upnp.IntTests/Services/ZoneGroupTopologyServiceTest.cs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..fb9dc23 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*.cs] +indent_size = 4 +trim_trailing_whitespace = true +max_line_length = off \ No newline at end of file diff --git a/README.md b/README.md index 2b2ab2c..8a2e873 100644 --- a/README.md +++ b/README.md @@ -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/ \ No newline at end of file diff --git a/src/ByteDev.Sonos.Console/SonosOperations.cs b/src/ByteDev.Sonos.Console/SonosOperations.cs index 7f5b053..3a38b69 100644 --- a/src/ByteDev.Sonos.Console/SonosOperations.cs +++ b/src/ByteDev.Sonos.Console/SonosOperations.cs @@ -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) diff --git a/src/ByteDev.Sonos.Ui/MainForm.Designer.cs b/src/ByteDev.Sonos.Ui/MainForm.Designer.cs index 941b938..686a7d6 100644 --- a/src/ByteDev.Sonos.Ui/MainForm.Designer.cs +++ b/src/ByteDev.Sonos.Ui/MainForm.Designer.cs @@ -28,249 +28,261 @@ protected override void Dispose(bool disposing) /// private void InitializeComponent() { - System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); - this.shutUpButton = new System.Windows.Forms.Button(); - this.ipAddressTextBox = new System.Windows.Forms.TextBox(); - this.intervalTextBox = new System.Windows.Forms.TextBox(); - this.stopButton = new System.Windows.Forms.Button(); - this.statusStrip = new System.Windows.Forms.StatusStrip(); - this.toolStripStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); - this.speakerIpLabel = new System.Windows.Forms.Label(); - this.intervalSecondsLabel = new System.Windows.Forms.Label(); - this.outputTextBox = new System.Windows.Forms.TextBox(); - this.setVolButton = new System.Windows.Forms.Button(); - this.volTextBox = new System.Windows.Forms.TextBox(); - this.scanButton = new System.Windows.Forms.Button(); - this.getDeviceButton = new System.Windows.Forms.Button(); - this.getVolumeButton = new System.Windows.Forms.Button(); - this.getQueueButton = new System.Windows.Forms.Button(); - this.removeQueueTrackButton = new System.Windows.Forms.Button(); - this.trackNumberTextBox = new System.Windows.Forms.TextBox(); - this.fadeButton = new System.Windows.Forms.Button(); - this.fadeTargetVolumeTextBox = new System.Windows.Forms.TextBox(); - this.rebootButton = new System.Windows.Forms.Button(); - this.statusStrip.SuspendLayout(); - this.SuspendLayout(); - // - // shutUpButton - // - this.shutUpButton.Location = new System.Drawing.Point(237, 470); - this.shutUpButton.Name = "shutUpButton"; - this.shutUpButton.Size = new System.Drawing.Size(75, 23); - this.shutUpButton.TabIndex = 0; - this.shutUpButton.Text = "Shut Up"; - this.shutUpButton.UseVisualStyleBackColor = true; - this.shutUpButton.Click += new System.EventHandler(this.shutUpButton_Click); - // - // ipAddressTextBox - // - this.ipAddressTextBox.Location = new System.Drawing.Point(80, 12); - this.ipAddressTextBox.Name = "ipAddressTextBox"; - this.ipAddressTextBox.Size = new System.Drawing.Size(120, 20); - this.ipAddressTextBox.TabIndex = 1; - this.ipAddressTextBox.Text = "192.168.1.105"; - // - // intervalTextBox - // - this.intervalTextBox.Location = new System.Drawing.Point(111, 470); - this.intervalTextBox.Name = "intervalTextBox"; - this.intervalTextBox.Size = new System.Drawing.Size(121, 20); - this.intervalTextBox.TabIndex = 2; - this.intervalTextBox.Text = "120"; - // - // stopButton - // - this.stopButton.Location = new System.Drawing.Point(318, 470); - this.stopButton.Name = "stopButton"; - this.stopButton.Size = new System.Drawing.Size(75, 23); - this.stopButton.TabIndex = 3; - this.stopButton.Text = "Stop"; - this.stopButton.UseVisualStyleBackColor = true; - this.stopButton.Click += new System.EventHandler(this.stopButton_Click); - // - // statusStrip - // - this.statusStrip.ImageScalingSize = new System.Drawing.Size(20, 20); - this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { + System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); + this.shutUpButton = new System.Windows.Forms.Button(); + this.ipAddressTextBox = new System.Windows.Forms.TextBox(); + this.intervalTextBox = new System.Windows.Forms.TextBox(); + this.stopButton = new System.Windows.Forms.Button(); + this.statusStrip = new System.Windows.Forms.StatusStrip(); + this.toolStripStatusLabel = new System.Windows.Forms.ToolStripStatusLabel(); + this.speakerIpLabel = new System.Windows.Forms.Label(); + this.intervalSecondsLabel = new System.Windows.Forms.Label(); + this.outputTextBox = new System.Windows.Forms.TextBox(); + this.setVolButton = new System.Windows.Forms.Button(); + this.volTextBox = new System.Windows.Forms.TextBox(); + this.scanButton = new System.Windows.Forms.Button(); + this.getDeviceButton = new System.Windows.Forms.Button(); + this.getVolumeButton = new System.Windows.Forms.Button(); + this.getQueueButton = new System.Windows.Forms.Button(); + this.removeQueueTrackButton = new System.Windows.Forms.Button(); + this.trackNumberTextBox = new System.Windows.Forms.TextBox(); + this.fadeButton = new System.Windows.Forms.Button(); + this.fadeTargetVolumeTextBox = new System.Windows.Forms.TextBox(); + this.rebootButton = new System.Windows.Forms.Button(); + this.getGroupsButton = new System.Windows.Forms.Button(); + this.statusStrip.SuspendLayout(); + this.SuspendLayout(); + // + // shutUpButton + // + this.shutUpButton.Location = new System.Drawing.Point(237, 470); + this.shutUpButton.Name = "shutUpButton"; + this.shutUpButton.Size = new System.Drawing.Size(75, 23); + this.shutUpButton.TabIndex = 0; + this.shutUpButton.Text = "Shut Up"; + this.shutUpButton.UseVisualStyleBackColor = true; + this.shutUpButton.Click += new System.EventHandler(this.shutUpButton_Click); + // + // ipAddressTextBox + // + this.ipAddressTextBox.Location = new System.Drawing.Point(80, 12); + this.ipAddressTextBox.Name = "ipAddressTextBox"; + this.ipAddressTextBox.Size = new System.Drawing.Size(120, 20); + this.ipAddressTextBox.TabIndex = 1; + this.ipAddressTextBox.Text = "192.168.1.105"; + // + // intervalTextBox + // + this.intervalTextBox.Location = new System.Drawing.Point(111, 470); + this.intervalTextBox.Name = "intervalTextBox"; + this.intervalTextBox.Size = new System.Drawing.Size(121, 20); + this.intervalTextBox.TabIndex = 2; + this.intervalTextBox.Text = "120"; + // + // stopButton + // + this.stopButton.Location = new System.Drawing.Point(318, 470); + this.stopButton.Name = "stopButton"; + this.stopButton.Size = new System.Drawing.Size(75, 23); + this.stopButton.TabIndex = 3; + this.stopButton.Text = "Stop"; + this.stopButton.UseVisualStyleBackColor = true; + this.stopButton.Click += new System.EventHandler(this.stopButton_Click); + // + // statusStrip + // + this.statusStrip.ImageScalingSize = new System.Drawing.Size(20, 20); + this.statusStrip.Items.AddRange(new System.Windows.Forms.ToolStripItem[] { this.toolStripStatusLabel}); - this.statusStrip.Location = new System.Drawing.Point(0, 499); - this.statusStrip.Name = "statusStrip"; - this.statusStrip.Size = new System.Drawing.Size(610, 22); - this.statusStrip.TabIndex = 4; - // - // toolStripStatusLabel - // - this.toolStripStatusLabel.Name = "toolStripStatusLabel"; - this.toolStripStatusLabel.Size = new System.Drawing.Size(0, 17); - // - // speakerIpLabel - // - this.speakerIpLabel.AutoSize = true; - this.speakerIpLabel.Location = new System.Drawing.Point(11, 15); - this.speakerIpLabel.Name = "speakerIpLabel"; - this.speakerIpLabel.Size = new System.Drawing.Size(63, 13); - this.speakerIpLabel.TabIndex = 5; - this.speakerIpLabel.Text = "Speaker IP:"; - // - // intervalSecondsLabel - // - this.intervalSecondsLabel.AutoSize = true; - this.intervalSecondsLabel.Location = new System.Drawing.Point(9, 473); - this.intervalSecondsLabel.Name = "intervalSecondsLabel"; - this.intervalSecondsLabel.Size = new System.Drawing.Size(96, 13); - this.intervalSecondsLabel.TabIndex = 6; - this.intervalSecondsLabel.Text = "Interval (Seconds):"; - // - // outputTextBox - // - this.outputTextBox.Location = new System.Drawing.Point(0, 44); - this.outputTextBox.Margin = new System.Windows.Forms.Padding(2); - this.outputTextBox.Multiline = true; - this.outputTextBox.Name = "outputTextBox"; - this.outputTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both; - this.outputTextBox.Size = new System.Drawing.Size(612, 315); - this.outputTextBox.TabIndex = 7; - this.outputTextBox.WordWrap = false; - // - // setVolButton - // - this.setVolButton.Location = new System.Drawing.Point(86, 395); - this.setVolButton.Margin = new System.Windows.Forms.Padding(2); - this.setVolButton.Name = "setVolButton"; - this.setVolButton.Size = new System.Drawing.Size(75, 23); - this.setVolButton.TabIndex = 8; - this.setVolButton.Text = "Set Volume"; - this.setVolButton.UseVisualStyleBackColor = true; - this.setVolButton.Click += new System.EventHandler(this.setVolButton_Click); - // - // volTextBox - // - this.volTextBox.Location = new System.Drawing.Point(7, 398); - this.volTextBox.Margin = new System.Windows.Forms.Padding(2); - this.volTextBox.Name = "volTextBox"; - this.volTextBox.Size = new System.Drawing.Size(76, 20); - this.volTextBox.TabIndex = 9; - this.volTextBox.Text = "0"; - // - // scanButton - // - this.scanButton.Location = new System.Drawing.Point(523, 10); - this.scanButton.Name = "scanButton"; - this.scanButton.Size = new System.Drawing.Size(75, 23); - this.scanButton.TabIndex = 10; - this.scanButton.Text = "Scan"; - this.scanButton.UseVisualStyleBackColor = true; - this.scanButton.Click += new System.EventHandler(this.scanButton_Click); - // - // getDeviceButton - // - this.getDeviceButton.Location = new System.Drawing.Point(206, 10); - this.getDeviceButton.Name = "getDeviceButton"; - this.getDeviceButton.Size = new System.Drawing.Size(75, 23); - this.getDeviceButton.TabIndex = 11; - this.getDeviceButton.Text = "Get Device"; - this.getDeviceButton.UseVisualStyleBackColor = true; - this.getDeviceButton.Click += new System.EventHandler(this.getDeviceButton_Click); - // - // getVolumeButton - // - this.getVolumeButton.Location = new System.Drawing.Point(166, 396); - this.getVolumeButton.Name = "getVolumeButton"; - this.getVolumeButton.Size = new System.Drawing.Size(75, 23); - this.getVolumeButton.TabIndex = 12; - this.getVolumeButton.Text = "Get Volume"; - this.getVolumeButton.UseVisualStyleBackColor = true; - this.getVolumeButton.Click += new System.EventHandler(this.getVolumeButton_Click); - // - // getQueueButton - // - this.getQueueButton.Location = new System.Drawing.Point(523, 377); - this.getQueueButton.Name = "getQueueButton"; - this.getQueueButton.Size = new System.Drawing.Size(75, 23); - this.getQueueButton.TabIndex = 13; - this.getQueueButton.Text = "Get Queue"; - this.getQueueButton.UseVisualStyleBackColor = true; - this.getQueueButton.Click += new System.EventHandler(this.getQueueButton_Click); - // - // removeQueueTrackButton - // - this.removeQueueTrackButton.Location = new System.Drawing.Point(523, 411); - this.removeQueueTrackButton.Name = "removeQueueTrackButton"; - this.removeQueueTrackButton.Size = new System.Drawing.Size(75, 23); - this.removeQueueTrackButton.TabIndex = 14; - this.removeQueueTrackButton.Text = "Rm Q Track"; - this.removeQueueTrackButton.UseVisualStyleBackColor = true; - this.removeQueueTrackButton.Click += new System.EventHandler(this.removeQueueTrackButton_Click); - // - // trackNumberTextBox - // - this.trackNumberTextBox.Location = new System.Drawing.Point(447, 413); - this.trackNumberTextBox.Name = "trackNumberTextBox"; - this.trackNumberTextBox.Size = new System.Drawing.Size(70, 20); - this.trackNumberTextBox.TabIndex = 15; - this.trackNumberTextBox.Text = "1"; - // - // fadeButton - // - this.fadeButton.Location = new System.Drawing.Point(86, 423); - this.fadeButton.Name = "fadeButton"; - this.fadeButton.Size = new System.Drawing.Size(75, 23); - this.fadeButton.TabIndex = 16; - this.fadeButton.Text = "Fade"; - this.fadeButton.UseVisualStyleBackColor = true; - this.fadeButton.Click += new System.EventHandler(this.fadeButton_Click); - // - // fadeTargetVolumeTextBox - // - this.fadeTargetVolumeTextBox.Location = new System.Drawing.Point(7, 426); - this.fadeTargetVolumeTextBox.Name = "fadeTargetVolumeTextBox"; - this.fadeTargetVolumeTextBox.Size = new System.Drawing.Size(76, 20); - this.fadeTargetVolumeTextBox.TabIndex = 17; - this.fadeTargetVolumeTextBox.Text = "10"; - // - // rebootButton - // - this.rebootButton.Location = new System.Drawing.Point(307, 10); - this.rebootButton.Name = "rebootButton"; - this.rebootButton.Size = new System.Drawing.Size(75, 23); - this.rebootButton.TabIndex = 18; - this.rebootButton.Text = "Reboot"; - this.rebootButton.UseVisualStyleBackColor = true; - this.rebootButton.Click += new System.EventHandler(this.rebootButton_Click); - // - // MainForm - // - this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); - this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; - this.ClientSize = new System.Drawing.Size(610, 521); - this.Controls.Add(this.rebootButton); - this.Controls.Add(this.fadeTargetVolumeTextBox); - this.Controls.Add(this.fadeButton); - this.Controls.Add(this.trackNumberTextBox); - this.Controls.Add(this.removeQueueTrackButton); - this.Controls.Add(this.getQueueButton); - this.Controls.Add(this.getVolumeButton); - this.Controls.Add(this.getDeviceButton); - this.Controls.Add(this.scanButton); - this.Controls.Add(this.volTextBox); - this.Controls.Add(this.setVolButton); - this.Controls.Add(this.outputTextBox); - this.Controls.Add(this.intervalSecondsLabel); - this.Controls.Add(this.speakerIpLabel); - this.Controls.Add(this.statusStrip); - this.Controls.Add(this.stopButton); - this.Controls.Add(this.intervalTextBox); - this.Controls.Add(this.ipAddressTextBox); - this.Controls.Add(this.shutUpButton); - this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); - this.MaximizeBox = false; - this.Name = "MainForm"; - this.Text = "Sonos UI"; - this.Load += new System.EventHandler(this.MainForm_Load); - this.statusStrip.ResumeLayout(false); - this.statusStrip.PerformLayout(); - this.ResumeLayout(false); - this.PerformLayout(); + this.statusStrip.Location = new System.Drawing.Point(0, 499); + this.statusStrip.Name = "statusStrip"; + this.statusStrip.Size = new System.Drawing.Size(610, 22); + this.statusStrip.TabIndex = 4; + // + // toolStripStatusLabel + // + this.toolStripStatusLabel.Name = "toolStripStatusLabel"; + this.toolStripStatusLabel.Size = new System.Drawing.Size(0, 17); + // + // speakerIpLabel + // + this.speakerIpLabel.AutoSize = true; + this.speakerIpLabel.Location = new System.Drawing.Point(11, 15); + this.speakerIpLabel.Name = "speakerIpLabel"; + this.speakerIpLabel.Size = new System.Drawing.Size(63, 13); + this.speakerIpLabel.TabIndex = 5; + this.speakerIpLabel.Text = "Speaker IP:"; + // + // intervalSecondsLabel + // + this.intervalSecondsLabel.AutoSize = true; + this.intervalSecondsLabel.Location = new System.Drawing.Point(9, 473); + this.intervalSecondsLabel.Name = "intervalSecondsLabel"; + this.intervalSecondsLabel.Size = new System.Drawing.Size(96, 13); + this.intervalSecondsLabel.TabIndex = 6; + this.intervalSecondsLabel.Text = "Interval (Seconds):"; + // + // outputTextBox + // + this.outputTextBox.Location = new System.Drawing.Point(0, 44); + this.outputTextBox.Margin = new System.Windows.Forms.Padding(2); + this.outputTextBox.Multiline = true; + this.outputTextBox.Name = "outputTextBox"; + this.outputTextBox.ScrollBars = System.Windows.Forms.ScrollBars.Both; + this.outputTextBox.Size = new System.Drawing.Size(612, 315); + this.outputTextBox.TabIndex = 7; + this.outputTextBox.WordWrap = false; + // + // setVolButton + // + this.setVolButton.Location = new System.Drawing.Point(86, 395); + this.setVolButton.Margin = new System.Windows.Forms.Padding(2); + this.setVolButton.Name = "setVolButton"; + this.setVolButton.Size = new System.Drawing.Size(75, 23); + this.setVolButton.TabIndex = 8; + this.setVolButton.Text = "Set Volume"; + this.setVolButton.UseVisualStyleBackColor = true; + this.setVolButton.Click += new System.EventHandler(this.setVolButton_Click); + // + // volTextBox + // + this.volTextBox.Location = new System.Drawing.Point(7, 398); + this.volTextBox.Margin = new System.Windows.Forms.Padding(2); + this.volTextBox.Name = "volTextBox"; + this.volTextBox.Size = new System.Drawing.Size(76, 20); + this.volTextBox.TabIndex = 9; + this.volTextBox.Text = "0"; + // + // scanButton + // + this.scanButton.Location = new System.Drawing.Point(523, 10); + this.scanButton.Name = "scanButton"; + this.scanButton.Size = new System.Drawing.Size(75, 23); + this.scanButton.TabIndex = 10; + this.scanButton.Text = "Scan"; + this.scanButton.UseVisualStyleBackColor = true; + this.scanButton.Click += new System.EventHandler(this.scanButton_Click); + // + // getDeviceButton + // + this.getDeviceButton.Location = new System.Drawing.Point(206, 10); + this.getDeviceButton.Name = "getDeviceButton"; + this.getDeviceButton.Size = new System.Drawing.Size(75, 23); + this.getDeviceButton.TabIndex = 11; + this.getDeviceButton.Text = "Get Device"; + this.getDeviceButton.UseVisualStyleBackColor = true; + this.getDeviceButton.Click += new System.EventHandler(this.getDeviceButton_Click); + // + // getVolumeButton + // + this.getVolumeButton.Location = new System.Drawing.Point(166, 396); + this.getVolumeButton.Name = "getVolumeButton"; + this.getVolumeButton.Size = new System.Drawing.Size(75, 23); + this.getVolumeButton.TabIndex = 12; + this.getVolumeButton.Text = "Get Volume"; + this.getVolumeButton.UseVisualStyleBackColor = true; + this.getVolumeButton.Click += new System.EventHandler(this.getVolumeButton_Click); + // + // getQueueButton + // + this.getQueueButton.Location = new System.Drawing.Point(523, 377); + this.getQueueButton.Name = "getQueueButton"; + this.getQueueButton.Size = new System.Drawing.Size(75, 23); + this.getQueueButton.TabIndex = 13; + this.getQueueButton.Text = "Get Queue"; + this.getQueueButton.UseVisualStyleBackColor = true; + this.getQueueButton.Click += new System.EventHandler(this.getQueueButton_Click); + // + // removeQueueTrackButton + // + this.removeQueueTrackButton.Location = new System.Drawing.Point(523, 411); + this.removeQueueTrackButton.Name = "removeQueueTrackButton"; + this.removeQueueTrackButton.Size = new System.Drawing.Size(75, 23); + this.removeQueueTrackButton.TabIndex = 14; + this.removeQueueTrackButton.Text = "Rm Q Track"; + this.removeQueueTrackButton.UseVisualStyleBackColor = true; + this.removeQueueTrackButton.Click += new System.EventHandler(this.removeQueueTrackButton_Click); + // + // trackNumberTextBox + // + this.trackNumberTextBox.Location = new System.Drawing.Point(447, 413); + this.trackNumberTextBox.Name = "trackNumberTextBox"; + this.trackNumberTextBox.Size = new System.Drawing.Size(70, 20); + this.trackNumberTextBox.TabIndex = 15; + this.trackNumberTextBox.Text = "1"; + // + // fadeButton + // + this.fadeButton.Location = new System.Drawing.Point(86, 423); + this.fadeButton.Name = "fadeButton"; + this.fadeButton.Size = new System.Drawing.Size(75, 23); + this.fadeButton.TabIndex = 16; + this.fadeButton.Text = "Fade"; + this.fadeButton.UseVisualStyleBackColor = true; + this.fadeButton.Click += new System.EventHandler(this.fadeButton_Click); + // + // fadeTargetVolumeTextBox + // + this.fadeTargetVolumeTextBox.Location = new System.Drawing.Point(7, 426); + this.fadeTargetVolumeTextBox.Name = "fadeTargetVolumeTextBox"; + this.fadeTargetVolumeTextBox.Size = new System.Drawing.Size(76, 20); + this.fadeTargetVolumeTextBox.TabIndex = 17; + this.fadeTargetVolumeTextBox.Text = "10"; + // + // rebootButton + // + this.rebootButton.Location = new System.Drawing.Point(307, 10); + this.rebootButton.Name = "rebootButton"; + this.rebootButton.Size = new System.Drawing.Size(75, 23); + this.rebootButton.TabIndex = 18; + this.rebootButton.Text = "Reboot"; + this.rebootButton.UseVisualStyleBackColor = true; + this.rebootButton.Click += new System.EventHandler(this.rebootButton_Click); + // + // getGroupsButton + // + this.getGroupsButton.Location = new System.Drawing.Point(290, 377); + this.getGroupsButton.Name = "getGroupsButton"; + this.getGroupsButton.Size = new System.Drawing.Size(103, 23); + this.getGroupsButton.TabIndex = 19; + this.getGroupsButton.Text = "Get Groups"; + this.getGroupsButton.UseVisualStyleBackColor = true; + this.getGroupsButton.Click += new System.EventHandler(this.getGroupsButton_Click); + // + // MainForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(610, 521); + this.Controls.Add(this.getGroupsButton); + this.Controls.Add(this.rebootButton); + this.Controls.Add(this.fadeTargetVolumeTextBox); + this.Controls.Add(this.fadeButton); + this.Controls.Add(this.trackNumberTextBox); + this.Controls.Add(this.removeQueueTrackButton); + this.Controls.Add(this.getQueueButton); + this.Controls.Add(this.getVolumeButton); + this.Controls.Add(this.getDeviceButton); + this.Controls.Add(this.scanButton); + this.Controls.Add(this.volTextBox); + this.Controls.Add(this.setVolButton); + this.Controls.Add(this.outputTextBox); + this.Controls.Add(this.intervalSecondsLabel); + this.Controls.Add(this.speakerIpLabel); + this.Controls.Add(this.statusStrip); + this.Controls.Add(this.stopButton); + this.Controls.Add(this.intervalTextBox); + this.Controls.Add(this.ipAddressTextBox); + this.Controls.Add(this.shutUpButton); + this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); + this.MaximizeBox = false; + this.Name = "MainForm"; + this.Text = "Sonos UI"; + this.Load += new System.EventHandler(this.MainForm_Load); + this.statusStrip.ResumeLayout(false); + this.statusStrip.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); } @@ -296,6 +308,7 @@ private void InitializeComponent() private System.Windows.Forms.Button fadeButton; private System.Windows.Forms.TextBox fadeTargetVolumeTextBox; private System.Windows.Forms.Button rebootButton; + private System.Windows.Forms.Button getGroupsButton; } } diff --git a/src/ByteDev.Sonos.Ui/MainForm.cs b/src/ByteDev.Sonos.Ui/MainForm.cs index f0a1a72..a432006 100644 --- a/src/ByteDev.Sonos.Ui/MainForm.cs +++ b/src/ByteDev.Sonos.Ui/MainForm.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; @@ -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(); diff --git a/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroup.cs b/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroup.cs new file mode 100644 index 0000000..6c6c1f6 --- /dev/null +++ b/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroup.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupMember.cs b/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupMember.cs new file mode 100644 index 0000000..75d2f17 --- /dev/null +++ b/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupMember.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupState.cs b/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupState.cs new file mode 100644 index 0000000..84463d2 --- /dev/null +++ b/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupState.cs @@ -0,0 +1,9 @@ +namespace ByteDev.Sonos.Upnp.Services.Models +{ + public class ZoneGroupState + { + public ZoneGroups ZoneGroups { get; set; } + + public object VanishedDevices { get; set; } + } +} \ No newline at end of file diff --git a/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupStateZoneGroups.cs b/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupStateZoneGroups.cs new file mode 100644 index 0000000..c286086 --- /dev/null +++ b/src/ByteDev.Sonos.Upnp/Services/Models/ZoneGroupStateZoneGroups.cs @@ -0,0 +1,10 @@ +using System.Xml.Serialization; + +namespace ByteDev.Sonos.Upnp.Services.Models +{ + public class ZoneGroups + { + [XmlElement("ZoneGroup")] + public ZoneGroup[] Items{ get; set; } + } +} \ No newline at end of file diff --git a/src/ByteDev.Sonos.Upnp/Services/ZoneGroupTopologyService.cs b/src/ByteDev.Sonos.Upnp/Services/ZoneGroupTopologyService.cs new file mode 100644 index 0000000..adada10 --- /dev/null +++ b/src/ByteDev.Sonos.Upnp/Services/ZoneGroupTopologyService.cs @@ -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 GetZoneGroupStateAsync() + { + var xmlResponse = await _upnpClient.InvokeFuncAsync("GetZoneGroupState"); + using (var responseStream = new StringReader(xmlResponse)) + { + var zoneGroupState = (ZoneGroupState) XmlSerializer.Deserialize(responseStream); + return zoneGroupState; + } + } + } +} \ No newline at end of file diff --git a/src/ByteDev.Sonos/SonosController.cs b/src/ByteDev.Sonos/SonosController.cs index 4fcc340..e1e3f63 100644 --- a/src/ByteDev.Sonos/SonosController.cs +++ b/src/ByteDev.Sonos/SonosController.cs @@ -1,4 +1,5 @@ -using System.Threading; + +using System.Threading; using System.Threading.Tasks; using ByteDev.Sonos.Models; using ByteDev.Sonos.Upnp.Services; @@ -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 @@ -177,5 +180,14 @@ public async Task SeekTrackAsync(SonosTrackNumber trackNumber) } #endregion + + #region Groups + + public async Task GetGroupsAsync() + { + return await _zoneGroupTopologyService.GetZoneGroupStateAsync(); + } + + #endregion } } \ No newline at end of file diff --git a/src/ByteDev.Sonos/SonosControllerFactory.cs b/src/ByteDev.Sonos/SonosControllerFactory.cs index 4314f79..08ea577 100644 --- a/src/ByteDev.Sonos/SonosControllerFactory.cs +++ b/src/ByteDev.Sonos/SonosControllerFactory.cs @@ -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)); } } } \ No newline at end of file diff --git a/tests/ByteDev.Sonos.Upnp.IntTests/Services/AvTransportServiceTest.cs b/tests/ByteDev.Sonos.Upnp.IntTests/Services/AvTransportServiceTest.cs index 63a2fe0..44f8bee 100644 --- a/tests/ByteDev.Sonos.Upnp.IntTests/Services/AvTransportServiceTest.cs +++ b/tests/ByteDev.Sonos.Upnp.IntTests/Services/AvTransportServiceTest.cs @@ -15,6 +15,7 @@ public class AvTransportServiceTest private SonosController _sonosController; private RenderingControlService _renderingControlService; private ContentDirectoryService _contentDirectoryService; + private ZoneGroupTopologyService _zoneGroupTopologyService; [SetUp] public void SetUp() @@ -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() diff --git a/tests/ByteDev.Sonos.Upnp.IntTests/Services/ZoneGroupTopologyServiceTest.cs b/tests/ByteDev.Sonos.Upnp.IntTests/Services/ZoneGroupTopologyServiceTest.cs new file mode 100644 index 0000000..343656e --- /dev/null +++ b/tests/ByteDev.Sonos.Upnp.IntTests/Services/ZoneGroupTopologyServiceTest.cs @@ -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)); + } + } +} \ No newline at end of file