From fd15d5b1d7348c9826f3ae11fcd8468383cafbc4 Mon Sep 17 00:00:00 2001 From: Michiel Post Date: Sun, 6 Aug 2023 17:38:24 +0200 Subject: [PATCH] Migrated HueBridgeDiscovery to HueApi --- src/HueApi.Tests/BridgeDiscoveryTests.cs | 9 + src/HueApi.Tests/HueApi.Tests.csproj | 6 +- .../BridgeLocator/HueBridgeDiscovery.cs | 184 ++++++++++++++++++ .../CancellationTokenSourceExtensions.cs | 21 ++ src/HueApi/HueApi.csproj | 2 +- src/HueApi/Models/Device.cs | 2 - src/HueApi/Models/Entertainment.cs | 3 - src/HueApi/Models/Light.cs | 3 - src/HueApi/Models/LightCommand.cs | 1 + .../Models/Responses/EventStreamResponse.cs | 2 - src/HueApi/Models/Room.cs | 3 - src/HueApi/Models/Sensors/LightLevel.cs | 2 - src/HueApi/Models/Zone.cs | 3 - 13 files changed, 219 insertions(+), 22 deletions(-) create mode 100644 src/HueApi/BridgeLocator/HueBridgeDiscovery.cs create mode 100644 src/HueApi/Extensions/CancellationTokenSourceExtensions.cs diff --git a/src/HueApi.Tests/BridgeDiscoveryTests.cs b/src/HueApi.Tests/BridgeDiscoveryTests.cs index 5f593c9..fc98144 100644 --- a/src/HueApi.Tests/BridgeDiscoveryTests.cs +++ b/src/HueApi.Tests/BridgeDiscoveryTests.cs @@ -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; diff --git a/src/HueApi.Tests/HueApi.Tests.csproj b/src/HueApi.Tests/HueApi.Tests.csproj index 432eda6..9d8e47c 100644 --- a/src/HueApi.Tests/HueApi.Tests.csproj +++ b/src/HueApi.Tests/HueApi.Tests.csproj @@ -10,9 +10,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/HueApi/BridgeLocator/HueBridgeDiscovery.cs b/src/HueApi/BridgeLocator/HueBridgeDiscovery.cs new file mode 100644 index 0000000..074084b --- /dev/null +++ b/src/HueApi/BridgeLocator/HueBridgeDiscovery.cs @@ -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 +{ + /// + /// 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 + /// + public static class HueBridgeDiscovery + { + /// + /// 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 + /// + /// General purpose approach for comprehensive search + /// Max awaited time if nothing found is maxNetwScanTimeout + /// Timeout for the fast locators (at least a few seconds, usually around 5 seconds) + /// Timeout for the slow network scan (at least a 30 seconds, to few minutes) + /// List of bridges found + public async static Task> 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>[] { + // 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 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(); + } + } + + /// + /// 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 + /// + /// Better approach for comprehensive search for smartphone environment + /// Max awaited time if nothing found is fastTimeout+maxNetwScanTimeout + /// Timeout for the fast locators (at least a few seconds, usually around 5 seconds) + /// Timeout for the slow network scan (at least a 30 seconds, to few minutes) + /// List of bridges found + public async static Task> 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>[] { + // 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 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(); + } + } + + /// + /// Discovery of Hue Bridges: + /// - Run only the fast locators (N-UPNP, MDNS, SSDP) in parallel + /// - Check first with N-UPNP, then MDNS/SSDP + /// + /// Best approach if network scan is not desirable + /// Timeout for the search (at least a few seconds, usually around 5 seconds) + /// List of bridges found + public async static Task> 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>[] { + // 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 result = new List(); + + // 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(); + } + } + + } +} diff --git a/src/HueApi/Extensions/CancellationTokenSourceExtensions.cs b/src/HueApi/Extensions/CancellationTokenSourceExtensions.cs new file mode 100644 index 0000000..6f20253 --- /dev/null +++ b/src/HueApi/Extensions/CancellationTokenSourceExtensions.cs @@ -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 + { + /// + /// Based on: https://stackoverflow.com/questions/31495411/a-call-to-cancellationtokensource-cancel-never-returns/31496340#31496340 + /// + /// + 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) + } + } +} diff --git a/src/HueApi/HueApi.csproj b/src/HueApi/HueApi.csproj index f1dd5fc..37fd38f 100644 --- a/src/HueApi/HueApi.csproj +++ b/src/HueApi/HueApi.csproj @@ -6,7 +6,7 @@ enable enable nullable - 1.2.0 + 1.3.0 Michiel Post For Clip v2 API. Open source library for interaction with the Philips Hue Bridge. Allows you to control your lights from C#. https://github.com/michielpost/Q42.HueApi diff --git a/src/HueApi/Models/Device.cs b/src/HueApi/Models/Device.cs index 9afcb26..e0c579d 100644 --- a/src/HueApi/Models/Device.cs +++ b/src/HueApi/Models/Device.cs @@ -34,8 +34,6 @@ public class Device : HueResource [JsonPropertyName("product_data")] public ProductData ProductData { get; set; } = new(); - [JsonPropertyName("services")] - public List Services { get; set; } = new(); } } diff --git a/src/HueApi/Models/Entertainment.cs b/src/HueApi/Models/Entertainment.cs index 208f519..ea89863 100644 --- a/src/HueApi/Models/Entertainment.cs +++ b/src/HueApi/Models/Entertainment.cs @@ -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; } diff --git a/src/HueApi/Models/Light.cs b/src/HueApi/Models/Light.cs index 2c79fa7..dcaaf9a 100644 --- a/src/HueApi/Models/Light.cs +++ b/src/HueApi/Models/Light.cs @@ -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!; diff --git a/src/HueApi/Models/LightCommand.cs b/src/HueApi/Models/LightCommand.cs index 50d51f6..ebea0ed 100644 --- a/src/HueApi/Models/LightCommand.cs +++ b/src/HueApi/Models/LightCommand.cs @@ -10,6 +10,7 @@ namespace HueApi /// /// For easy migration from Q42.HueApi /// + [Obsolete("Replace with: UpdateLight")] public class LightCommand : UpdateLight { } diff --git a/src/HueApi/Models/Responses/EventStreamResponse.cs b/src/HueApi/Models/Responses/EventStreamResponse.cs index 7ef392d..f362d8c 100644 --- a/src/HueApi/Models/Responses/EventStreamResponse.cs +++ b/src/HueApi/Models/Responses/EventStreamResponse.cs @@ -10,8 +10,6 @@ namespace HueApi.Models.Responses { public class EventStreamData : HueResource { - [JsonPropertyName("owner")] - public ResourceIdentifier? Owner { get; set; } } diff --git a/src/HueApi/Models/Room.cs b/src/HueApi/Models/Room.cs index 003af25..5810af4 100644 --- a/src/HueApi/Models/Room.cs +++ b/src/HueApi/Models/Room.cs @@ -15,8 +15,5 @@ public class Room : HueResource [JsonPropertyName("grouped_services")] public List GroupedServices { get; set; } = new(); - [JsonPropertyName("services")] - public List Services { get; set; } = new(); - } } diff --git a/src/HueApi/Models/Sensors/LightLevel.cs b/src/HueApi/Models/Sensors/LightLevel.cs index 9f5717c..cf04734 100644 --- a/src/HueApi/Models/Sensors/LightLevel.cs +++ b/src/HueApi/Models/Sensors/LightLevel.cs @@ -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 diff --git a/src/HueApi/Models/Zone.cs b/src/HueApi/Models/Zone.cs index 41ec308..e4506b3 100644 --- a/src/HueApi/Models/Zone.cs +++ b/src/HueApi/Models/Zone.cs @@ -19,8 +19,5 @@ public class Zone : HueResource [JsonPropertyName("grouped_services")] public List GroupedServices { get; set; } = new(); - [JsonPropertyName("services")] - public List Services { get; set; } = new(); - } }