Skip to content

Commit

Permalink
Migrated HueBridgeDiscovery to HueApi
Browse files Browse the repository at this point in the history
  • Loading branch information
michielpost committed Aug 6, 2023
1 parent 9bed86d commit fd15d5b
Show file tree
Hide file tree
Showing 13 changed files with 219 additions and 22 deletions.
9 changes: 9 additions & 0 deletions src/HueApi.Tests/BridgeDiscoveryTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ await Task.WhenAll(new Task[] {
});
}

[TestMethod]
public async Task TestComplete()
{
var result = await HueBridgeDiscovery.CompleteDiscoveryAsync(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));

Assert.IsNotNull(result);
}


private async Task TestBridgeLocatorWithTimeout(IBridgeLocator locator, TimeSpan timeout)
{
var startTime = DateTime.Now;
Expand Down
6 changes: 3 additions & 3 deletions src/HueApi.Tests/HueApi.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.2" />
<PackageReference Include="MSTest.TestAdapter" Version="3.0.4" />
<PackageReference Include="MSTest.TestFramework" Version="3.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.7.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
<PackageReference Include="coverlet.collector" Version="6.0.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
184 changes: 184 additions & 0 deletions src/HueApi/BridgeLocator/HueBridgeDiscovery.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using HueApi.Extensions;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace HueApi.BridgeLocator
{
/// <summary>
/// Some Hue Bridge Discovery approaches using the Q42.HueApi
/// See https://developers.meethue.com/develop/application-design-guidance/hue-bridge-discovery/
/// Based on code by @Indigo744 https://github.com/Q42/Q42.HueApi/issues/198#issuecomment-564011445
/// </summary>
public static class HueBridgeDiscovery
{
/// <summary>
/// Discovery of Hue Bridges:
/// - Run all locators (N-UPNP, MDNS, SSDP, network scan) in parallel
/// - Check first with N-UPNP, then MDNS/SSDP
/// - If nothing found, continue with network scan
/// </summary>
/// <remarks>General purpose approach for comprehensive search</remarks>
/// <remarks>Max awaited time if nothing found is maxNetwScanTimeout</remarks>
/// <param name="fastTimeout">Timeout for the fast locators (at least a few seconds, usually around 5 seconds)</param>
/// <param name="maxNetwScanTimeout">Timeout for the slow network scan (at least a 30 seconds, to few minutes)</param>
/// <returns>List of bridges found</returns>
public async static Task<List<LocatedBridge>> CompleteDiscoveryAsync(TimeSpan fastTimeout, TimeSpan maxNetwScanTimeout)
{
using (var fastLocatorsCancelSrc = new CancellationTokenSource(fastTimeout))
using (var slowNetwScanCancelSrc = new CancellationTokenSource(maxNetwScanTimeout))
{
// Start all tasks in parallel without awaiting

// Pack all fast locators in an array so we can await them in order
var fastLocateTask = new Task<IEnumerable<LocatedBridge>>[] {
// N-UPNP is the fastest (only one endpoint to check)
new HttpBridgeLocator().LocateBridgesAsync(fastLocatorsCancelSrc.Token),
// MDNS is the most reliable for bridge V2
new MdnsBridgeLocator().LocateBridgesAsync(fastLocatorsCancelSrc.Token),
// SSDP is older but works for bridge V1 & V2
new SsdpBridgeLocator().LocateBridgesAsync(fastLocatorsCancelSrc.Token),
};

// The network scan locator is clearly the slowest
var slowNetwScanTask = new LocalNetworkScanBridgeLocator().LocateBridgesAsync(slowNetwScanCancelSrc.Token);

IEnumerable<LocatedBridge> result;

// We will loop through the fast locators and await each one in order
foreach (var fastTask in fastLocateTask)
{
// Await this task to get its result
result = await fastTask;

// Check if it contains anything
if (result.Any())
{
// Cancel all remaining tasks and return
fastLocatorsCancelSrc.CancelWithBackgroundContinuations();
slowNetwScanCancelSrc.CancelWithBackgroundContinuations();

return result.ToList();
}
else
{
// Nothing found using this locator
}
}

// All fast locators failed, we wait for the network scan to complete and return whatever we found
result = await slowNetwScanTask;
return result.ToList();
}
}

/// <summary>
/// Discovery of Hue Bridges:
/// - Run all fast locators (N-UPNP, MDNS, SSDP) in parallel
/// - Check first with N-UPNP, then MDNS/SSDP after 5 seconds
/// - If nothing found, run network scan up to 1 minute
/// </summary>
/// <remarks>Better approach for comprehensive search for smartphone environment</remarks>
/// <remarks>Max awaited time if nothing found is fastTimeout+maxNetwScanTimeout</remarks>
/// <param name="fastTimeout">Timeout for the fast locators (at least a few seconds, usually around 5 seconds)</param>
/// <param name="maxNetwScanTimeout">Timeout for the slow network scan (at least a 30 seconds, to few minutes)</param>
/// <returns>List of bridges found</returns>
public async static Task<List<LocatedBridge>> FastDiscoveryWithNetworkScanFallbackAsync(TimeSpan fastTimeout, TimeSpan maxNetwScanTimeout)
{
using (var fastLocatorsCancelSrc = new CancellationTokenSource(fastTimeout))
{
// Start all tasks in parallel without awaiting

// Pack all fast locators in an array so we can await them in order
var fastLocateTask = new Task<IEnumerable<LocatedBridge>>[] {
// N-UPNP is the fastest (only one endpoint to check)
new HttpBridgeLocator().LocateBridgesAsync(fastLocatorsCancelSrc.Token),
// MDNS is the most reliable for bridge V2
new MdnsBridgeLocator().LocateBridgesAsync(fastLocatorsCancelSrc.Token),
// SSDP is older but works for bridge V1 & V2
new SsdpBridgeLocator().LocateBridgesAsync(fastLocatorsCancelSrc.Token),
};

IEnumerable<LocatedBridge> result;

// We will loop through the fast locators and await each one in order
foreach (var fastTask in fastLocateTask)
{
// Await this task to get its result
result = await fastTask;

// Check if it contains anything
if (result.Any())
{
// Cancel all remaining tasks and return
fastLocatorsCancelSrc.CancelWithBackgroundContinuations();

return result.ToList();
}
else
{
// Nothing found using this locator
}
}

// All fast locators failed, let's try the network scan and return whatever we found
result = await new LocalNetworkScanBridgeLocator().LocateBridgesAsync(maxNetwScanTimeout);
return result.ToList();
}
}

/// <summary>
/// Discovery of Hue Bridges:
/// - Run only the fast locators (N-UPNP, MDNS, SSDP) in parallel
/// - Check first with N-UPNP, then MDNS/SSDP
/// </summary>
/// <remarks>Best approach if network scan is not desirable</remarks>
/// <param name="timeout">Timeout for the search (at least a few seconds, usually around 5 seconds)</param>
/// <returns>List of bridges found</returns>
public async static Task<List<LocatedBridge>> FastDiscoveryAsync(TimeSpan timeout)
{
using (var fastLocatorsCancelSrc = new CancellationTokenSource(timeout))
{
// Start all tasks in parallel without awaiting

// Pack all fast locators in an array so we can await them in order
var fastLocateTask = new Task<IEnumerable<LocatedBridge>>[] {
// N-UPNP is the fastest (only one endpoint to check)
new HttpBridgeLocator().LocateBridgesAsync(fastLocatorsCancelSrc.Token),
// MDNS is the most reliable for bridge V2
new MdnsBridgeLocator().LocateBridgesAsync(fastLocatorsCancelSrc.Token),
// SSDP is older but works for bridge V1 & V2
new SsdpBridgeLocator().LocateBridgesAsync(fastLocatorsCancelSrc.Token),
};

IEnumerable<LocatedBridge> result = new List<LocatedBridge>();

// We will loop through the fast locators and await each one in order
foreach (var fastTask in fastLocateTask)
{
// Await this task to get its result
result = await fastTask;

// Check if it contains anything
if (result.Any())
{
// Cancel all remaining tasks and break
fastLocatorsCancelSrc.CancelWithBackgroundContinuations();

break;
}
else
{
// Nothing found using this locator
}
}

return result.ToList();
}
}

}
}
21 changes: 21 additions & 0 deletions src/HueApi/Extensions/CancellationTokenSourceExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace HueApi.Extensions
{
public static class CancellationTokenSourceExtensions
{
/// <summary>
/// Based on: https://stackoverflow.com/questions/31495411/a-call-to-cancellationtokensource-cancel-never-returns/31496340#31496340
/// </summary>
/// <param name=""></param>
public static void CancelWithBackgroundContinuations(this CancellationTokenSource cancellationTokenSource)
{
Task.Run(() => cancellationTokenSource.Cancel());
cancellationTokenSource.Token.WaitHandle.WaitOne(); // make sure to only continue when the cancellation completed (without waiting for all the callbacks)
}
}
}
2 changes: 1 addition & 1 deletion src/HueApi/HueApi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
<Version>1.2.0</Version>
<Version>1.3.0</Version>
<Authors>Michiel Post</Authors>
<Description>For Clip v2 API. Open source library for interaction with the Philips Hue Bridge. Allows you to control your lights from C#.</Description>
<PackageProjectUrl>https://github.com/michielpost/Q42.HueApi</PackageProjectUrl>
Expand Down
2 changes: 0 additions & 2 deletions src/HueApi/Models/Device.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ public class Device : HueResource
[JsonPropertyName("product_data")]
public ProductData ProductData { get; set; } = new();

[JsonPropertyName("services")]
public List<ResourceIdentifier> Services { get; set; } = new();

}
}
3 changes: 0 additions & 3 deletions src/HueApi/Models/Entertainment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ namespace HueApi.Models
{
public class Entertainment : HueResource
{
[JsonPropertyName("owner")]
public ResourceIdentifier? Owner { get; set; }

[JsonPropertyName("proxy")]
public bool Proxy { get; set; }

Expand Down
3 changes: 0 additions & 3 deletions src/HueApi/Models/Light.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ namespace HueApi.Models
{
public class Light : HueResource
{
[JsonPropertyName("owner")]
public ResourceIdentifier Owner { get; set; } = default!;

[JsonPropertyName("on")]
public On On { get; set; } = default!;

Expand Down
1 change: 1 addition & 0 deletions src/HueApi/Models/LightCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ namespace HueApi
/// <summary>
/// For easy migration from Q42.HueApi
/// </summary>
[Obsolete("Replace with: UpdateLight")]
public class LightCommand : UpdateLight
{
}
Expand Down
2 changes: 0 additions & 2 deletions src/HueApi/Models/Responses/EventStreamResponse.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ namespace HueApi.Models.Responses
{
public class EventStreamData : HueResource
{
[JsonPropertyName("owner")]
public ResourceIdentifier? Owner { get; set; }

}

Expand Down
3 changes: 0 additions & 3 deletions src/HueApi/Models/Room.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,5 @@ public class Room : HueResource
[JsonPropertyName("grouped_services")]
public List<ResourceIdentifier> GroupedServices { get; set; } = new();

[JsonPropertyName("services")]
public List<ResourceIdentifier> Services { get; set; } = new();

}
}
2 changes: 0 additions & 2 deletions src/HueApi/Models/Sensors/LightLevel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ public class LightLevel : HueResource
[JsonPropertyName("light")]
public Light Light { get; set; } = default!;

[JsonPropertyName("owner")]
public ResourceIdentifier? Owner { get; set; }
}

public class Light
Expand Down
3 changes: 0 additions & 3 deletions src/HueApi/Models/Zone.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,5 @@ public class Zone : HueResource
[JsonPropertyName("grouped_services")]
public List<ResourceIdentifier> GroupedServices { get; set; } = new();

[JsonPropertyName("services")]
public List<ResourceIdentifier> Services { get; set; } = new();

}
}

0 comments on commit fd15d5b

Please sign in to comment.