Add MusicCast discovery service
This commit is contained in:
@@ -15,13 +15,7 @@ namespace MusicCast.Net.Api.Client.Models
|
|||||||
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
|
/// <summary>Stores additional data not described in the OpenAPI description found when deserializing. Can be used for serialization as well.</summary>
|
||||||
public IDictionary<string, object> AdditionalData { get; set; }
|
public IDictionary<string, object> AdditionalData { get; set; }
|
||||||
/// <summary>The response_code property</summary>
|
/// <summary>The response_code property</summary>
|
||||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
public int? ResponseCode { get; set; }
|
||||||
#nullable enable
|
|
||||||
public string? ResponseCode { get; set; }
|
|
||||||
#nullable restore
|
|
||||||
#else
|
|
||||||
public string ResponseCode { get; set; }
|
|
||||||
#endif
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Instantiates a new <see cref="global::MusicCast.Net.Api.Client.Models.BaseResponse"/> and sets the default values.
|
/// Instantiates a new <see cref="global::MusicCast.Net.Api.Client.Models.BaseResponse"/> and sets the default values.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -47,7 +41,7 @@ namespace MusicCast.Net.Api.Client.Models
|
|||||||
{
|
{
|
||||||
return new Dictionary<string, Action<IParseNode>>
|
return new Dictionary<string, Action<IParseNode>>
|
||||||
{
|
{
|
||||||
{ "response_code", n => { ResponseCode = n.GetStringValue(); } },
|
{ "response_code", n => { ResponseCode = n.GetIntValue(); } },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -57,7 +51,7 @@ namespace MusicCast.Net.Api.Client.Models
|
|||||||
public virtual void Serialize(ISerializationWriter writer)
|
public virtual void Serialize(ISerializationWriter writer)
|
||||||
{
|
{
|
||||||
_ = writer ?? throw new ArgumentNullException(nameof(writer));
|
_ = writer ?? throw new ArgumentNullException(nameof(writer));
|
||||||
writer.WriteStringValue("response_code", ResponseCode);
|
writer.WriteIntValue("response_code", ResponseCode);
|
||||||
writer.WriteAdditionalData(AdditionalData);
|
writer.WriteAdditionalData(AdditionalData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,3 @@
|
|||||||
#!/usr/bin/fish
|
#!/usr/bin/fish
|
||||||
|
|
||||||
kiota.exe generate -l Csharp -c MusicCastApiClient -n MusicCast.Net.Api.Client -d MusicCast.Net.Api.Server.json -o . --exclude-backward-compatible
|
kiota.exe generate -l Csharp -c MusicCastApiClient -n MusicCast.Net.Api.Client -d ../MusicCast.Net.Api.Server/MusicCast.Net.Api.Server.json -o . --exclude-backward-compatible
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"descriptionHash": "C948E365F06EF135D929D78013E8AA08B1DD90FE86A80A3C7F3495672B6F1C32F98B1AAA7F84A151FAB4B0B8D12DB49F46CCCA14DE96A06143E25983F49846AA",
|
"descriptionHash": "54CA7AB3C7F7B94748F3D15ED096DB34D3A70EA1BC52DF9436D08BC31C7CCCFAC79F1FE2861D190E1915FE1C3C4036BDF1A1D6FB1CBACED8866663BC9D708742",
|
||||||
"descriptionLocation": "MusicCast.Net.Api.Server.json",
|
"descriptionLocation": "../MusicCast.Net.Api.Server/MusicCast.Net.Api.Server.json",
|
||||||
"lockFileVersion": "1.0.0",
|
"lockFileVersion": "1.0.0",
|
||||||
"kiotaVersion": "1.26.1",
|
"kiotaVersion": "1.26.1",
|
||||||
"clientClassName": "MusicCastApiClient",
|
"clientClassName": "MusicCastApiClient",
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ namespace MusicCast.Net.Api.Server.Models;
|
|||||||
|
|
||||||
public class BaseResponse
|
public class BaseResponse
|
||||||
{
|
{
|
||||||
public string response_code { get; set; }
|
public int response_code { get; set; }
|
||||||
}
|
}
|
||||||
@@ -1320,7 +1320,8 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"response_code": {
|
"response_code": {
|
||||||
"type": "string"
|
"type": "integer",
|
||||||
|
"format": "int32"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,7 +7,11 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MusicCast.Net.Api.Client\MusicCast.Net.Api.Client.csproj" PrivateAssets="*" />
|
<PackageReference Include="Microsoft.Kiota.Bundle" Version="1.17.3" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MusicCast.Net.Api.Client\MusicCast.Net.Api.Client.csproj" PrivateAssets="All" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
53
MusicCast.Net.Client/MusicCastClient.cs
Normal file
53
MusicCast.Net.Client/MusicCastClient.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using Microsoft.Kiota.Abstractions.Authentication;
|
||||||
|
using Microsoft.Kiota.Http.HttpClientLibrary;
|
||||||
|
using MusicCast.Net.Api.Client;
|
||||||
|
using MusicCast.Net.Api.Client.Models;
|
||||||
|
using MusicCast.Net.Api.Client.YamahaExtendedControl.V1.Main.SetPower;
|
||||||
|
|
||||||
|
namespace MusicCast.Net.Client;
|
||||||
|
|
||||||
|
public class MusicCastClient : IDisposable
|
||||||
|
{
|
||||||
|
private readonly string _deviceAddress;
|
||||||
|
private readonly HttpClientRequestAdapter _apiClientRequestAdaptor;
|
||||||
|
private readonly MusicCastApiClient _apiClient;
|
||||||
|
|
||||||
|
public MusicCastClient(string deviceAddress)
|
||||||
|
{
|
||||||
|
_deviceAddress = deviceAddress;
|
||||||
|
|
||||||
|
_apiClientRequestAdaptor = new HttpClientRequestAdapter(new AnonymousAuthenticationProvider())
|
||||||
|
{
|
||||||
|
BaseUrl = _deviceAddress
|
||||||
|
};
|
||||||
|
_apiClient = new MusicCastApiClient(_apiClientRequestAdaptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> PowerOn()
|
||||||
|
{
|
||||||
|
var response = await _apiClient.YamahaExtendedControl.V1.Main.SetPower.GetAsync(r =>
|
||||||
|
r.QueryParameters.Power = GetPowerQueryParameterType.On
|
||||||
|
);
|
||||||
|
|
||||||
|
return IsSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<bool> PowerOff()
|
||||||
|
{
|
||||||
|
var response = await _apiClient.YamahaExtendedControl.V1.Main.SetPower.GetAsync(r =>
|
||||||
|
r.QueryParameters.Power = GetPowerQueryParameterType.Standby
|
||||||
|
);
|
||||||
|
|
||||||
|
return IsSuccess(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsSuccess(BaseResponse? response)
|
||||||
|
{
|
||||||
|
return response is { ResponseCode: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_apiClientRequestAdaptor.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
162
MusicCast.Net.Client/MusicCastDiscoveryService.cs
Normal file
162
MusicCast.Net.Client/MusicCastDiscoveryService.cs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
using System.Net;
|
||||||
|
using System.Net.NetworkInformation;
|
||||||
|
using System.Net.Sockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using System.Xml.XPath;
|
||||||
|
|
||||||
|
namespace MusicCast.Net.Client;
|
||||||
|
|
||||||
|
public partial class MusicCastDiscoveryService
|
||||||
|
{
|
||||||
|
private static readonly byte[] MulticastPacket = """
|
||||||
|
M-SEARCH * HTTP/1.1
|
||||||
|
HOST: 239.255.255.250:1900
|
||||||
|
MAN: "ssdp:discover"
|
||||||
|
ST: urn:schemas-upnp-org:device:MediaRenderer:1
|
||||||
|
MX: 1
|
||||||
|
|
||||||
|
|
||||||
|
"""u8.ToArray();
|
||||||
|
|
||||||
|
private static readonly HttpClient _httpClient = new();
|
||||||
|
|
||||||
|
private const string LocationUrlGroupName = "url";
|
||||||
|
[GeneratedRegex(@$"Location:\s(?<{LocationUrlGroupName}>http[s]?:\/\/.+\.xml)", RegexOptions.IgnoreCase)]
|
||||||
|
private static partial Regex LocationRegex();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the ip address associated with the current local network.
|
||||||
|
/// If multiple networks cards are installed it only returns the first one.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public IPAddress? GetLocalSubnetAddress()
|
||||||
|
{
|
||||||
|
// Get a list of all network interfaces (usually one per network card, dialup, and VPN connection)
|
||||||
|
var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
|
||||||
|
|
||||||
|
foreach (NetworkInterface network in networkInterfaces)
|
||||||
|
{
|
||||||
|
// Read the IP configuration for each network
|
||||||
|
IPInterfaceProperties properties = network.GetIPProperties();
|
||||||
|
|
||||||
|
if (network.NetworkInterfaceType is NetworkInterfaceType.Ethernet or NetworkInterfaceType.Wireless80211 &&
|
||||||
|
network.OperationalStatus == OperationalStatus.Up &&
|
||||||
|
!network.Description.Contains("virtual", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!network.Description.Contains("pseudo", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Each network interface may have multiple IP addresses
|
||||||
|
foreach (UnicastIPAddressInformation address in properties.UnicastAddresses)
|
||||||
|
{
|
||||||
|
// We're only interested in IPv4 addresses for now
|
||||||
|
if (address.Address.AddressFamily != AddressFamily.InterNetwork)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Ignore loopback addresses (e.g., 127.0.0.1)
|
||||||
|
if (IPAddress.IsLoopback(address.Address))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
return address.Address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<HashSet<string>> GetMusicCastDeviceAddresses(IPAddress bindAddress)
|
||||||
|
=> GetMusicCastDeviceAddresses(bindAddress, TimeSpan.FromSeconds(2));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scan for MusicCast devices on the network associated with the bind address. Only return devices that support
|
||||||
|
/// the YamahaExtendedControl v1 api
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bindAddress"></param>
|
||||||
|
/// <param name="scanDuration">Duration of the scan</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<HashSet<string>> GetMusicCastDeviceAddresses(IPAddress bindAddress, TimeSpan scanDuration)
|
||||||
|
{
|
||||||
|
using var client = new UdpClient(AddressFamily.InterNetwork);
|
||||||
|
|
||||||
|
client.Client.Bind(new IPEndPoint(bindAddress, 0));
|
||||||
|
client.JoinMulticastGroup(IPAddress.Parse("239.255.255.250"));
|
||||||
|
|
||||||
|
await client.SendAsync(MulticastPacket, "239.255.255.250", 1900);
|
||||||
|
await client.SendAsync(MulticastPacket, "239.255.255.250", 1900);
|
||||||
|
await client.SendAsync(MulticastPacket, "239.255.255.250", 1900);
|
||||||
|
|
||||||
|
var musicCastDevices = new HashSet<string>();
|
||||||
|
|
||||||
|
// Start scanning
|
||||||
|
var cancellationToken = new CancellationTokenSource(scanDuration).Token;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
var received = await client.ReceiveAsync(cancellationToken);
|
||||||
|
var message = Encoding.UTF8.GetString(received.Buffer);
|
||||||
|
|
||||||
|
// Try to find the location of the DLNA description file
|
||||||
|
var match = LocationRegex().Match(message);
|
||||||
|
|
||||||
|
if (!match.Success || !match.Groups.TryGetValue(LocationUrlGroupName, out var group))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var location = group.Value;
|
||||||
|
|
||||||
|
var apiUrl = await GetApiUrlFromDlnaDescriptionFile(location, cancellationToken);
|
||||||
|
|
||||||
|
if (apiUrl is not null)
|
||||||
|
{
|
||||||
|
musicCastDevices.Add(apiUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Normal flow after scan duration
|
||||||
|
}
|
||||||
|
|
||||||
|
return musicCastDevices;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<string?> GetApiUrlFromDlnaDescriptionFile(string location, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await _httpClient.GetAsync(location, cancellationToken);
|
||||||
|
await using var descriptionFileStream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
|
||||||
|
var document = await XDocument.LoadAsync(descriptionFileStream, LoadOptions.None, cancellationToken);
|
||||||
|
XNamespace yamahaNamespace = "urn:schemas-yamaha-com:device-1-0";
|
||||||
|
|
||||||
|
var urlBaseNode = document.Descendants(yamahaNamespace + "X_URLBase").FirstOrDefault();
|
||||||
|
|
||||||
|
if (urlBaseNode is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var yamahaControlUrlNodes = document.Descendants(yamahaNamespace + "X_yxcControlURL");
|
||||||
|
|
||||||
|
foreach (var yamahaControlUrlNode in yamahaControlUrlNodes)
|
||||||
|
{
|
||||||
|
if (yamahaControlUrlNode is { Value: "/YamahaExtendedControl/v1/" })
|
||||||
|
{
|
||||||
|
return urlBaseNode.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\MusicCast.Net.Api.Client\MusicCast.Net.Api.Client.csproj" PrivateAssets="*" />
|
<ProjectReference Include="..\MusicCast.Net.Client\MusicCast.Net.Client.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -3,37 +3,22 @@
|
|||||||
// API requires no authentication, so use the anonymous
|
// API requires no authentication, so use the anonymous
|
||||||
// authentication provider
|
// authentication provider
|
||||||
|
|
||||||
using System.Text.Json;
|
using MusicCast.Net.Client;
|
||||||
using Microsoft.Kiota.Abstractions.Authentication;
|
|
||||||
using Microsoft.Kiota.Http.HttpClientLibrary;
|
|
||||||
using MusicCast.Net.Api.Client;
|
|
||||||
using MusicCast.Net.Api.Client.YamahaExtendedControl.V1.Main.SetPower;
|
|
||||||
|
|
||||||
// API requires no authentication, so use the anonymous
|
// Get device IP Address
|
||||||
// authentication provider
|
var discoveryService = new MusicCastDiscoveryService();
|
||||||
var authProvider = new AnonymousAuthenticationProvider();
|
|
||||||
|
|
||||||
// Create request adapter using the HttpClient-based implementation
|
var localIpAddress = discoveryService.GetLocalSubnetAddress()!;
|
||||||
using var adapter = new HttpClientRequestAdapter(authProvider);
|
|
||||||
adapter.BaseUrl = "http://192.168.129.21";
|
|
||||||
|
|
||||||
var client = new MusicCastApiClient(adapter);
|
var deviceAddresses = await discoveryService.GetMusicCastDeviceAddresses(localIpAddress);
|
||||||
|
|
||||||
var status = await client.YamahaExtendedControl.V1.Main.GetStatus.GetAsync();
|
foreach (var deviceAddress in deviceAddresses)
|
||||||
Console.WriteLine(status!.Power);
|
{
|
||||||
|
Console.WriteLine(deviceAddress);
|
||||||
|
}
|
||||||
|
|
||||||
await client.YamahaExtendedControl.V1.Main.SetPower.GetAsync(conf => conf.QueryParameters.Power = GetPowerQueryParameterType.On);
|
// Create and use client
|
||||||
|
var client = new MusicCastClient(deviceAddresses.First());
|
||||||
|
|
||||||
await Task.Delay(5000);
|
Console.WriteLine(await client.PowerOff());
|
||||||
|
|
||||||
status = await client.YamahaExtendedControl.V1.Main.GetStatus.GetAsync();
|
|
||||||
Console.WriteLine(status!.Power);
|
|
||||||
|
|
||||||
await client.YamahaExtendedControl.V1.Main.SetPower.GetAsync(r =>
|
|
||||||
r.QueryParameters.Power = GetPowerQueryParameterType.Standby
|
|
||||||
);
|
|
||||||
|
|
||||||
await Task.Delay(5000);
|
|
||||||
|
|
||||||
status = await client.YamahaExtendedControl.V1.Main.GetStatus.GetAsync();
|
|
||||||
Console.WriteLine(status!.Power);
|
|
||||||
Reference in New Issue
Block a user