michielpost / Q42.HueApi

C# helper library to talk to the Philips Hue bridge
MIT License
411 stars 114 forks source link

Add light to EntertainmentGroup en re-activate streaming #253

Closed TripleNico closed 2 years ago

TripleNico commented 2 years ago

Hi Mark,

Today i created a function where a user can add or remove a light to a Entertainmentgroup using UpdateGroupAsync . After the call i noticed than the bridge stops streaming. So i think that we have to setup the Entertainment again? Or is there a simple call like UpdateStreamingGroup ?

michielpost commented 2 years ago

There's no special call to update a streaming group while it's streaming. If the bridge stops streaming while you update the group, it's best to restart streaming manually.

TripleNico commented 2 years ago

hi @michielpost thanks for your swift reply. Alright if the bridge stops streaming what would be the "correct" way to recover this? Is that by doing the same steps as noted here: https://github.com/Q42/Q42.HueApi/blob/master/EntertainmentApi.md#connect-to-an-entertainment-group or is there an simpler way?

michielpost commented 2 years ago

Yes, safest thing is to follow these steps to setup a new fresh connection.

Calling var entGroup = new StreamingGroup(group.Locations); needs all light locations. So it's important to have up to date group info when you just added a new light to the group.

Normally, if the connection is lost you might be able to reconnect with this:

client.Close()
await client.Connect(group.Id);
client.AutoUpdate(entGroup, 50);

But creating a new client is safer, that way you're sure all internal state is reset.

TripleNico commented 2 years ago

Alright i will do some testing to see what the best result is in our case.

Another question, if a light is added to an entertainment group should a location be also set? Or is it just add, re-init streaming and done?

michielpost commented 2 years ago

A location is needed for effects that use it. For location aware effects, like a moving light from left to right. If it's not set, the location is probably 0,0, so that might not be what you want.

TripleNico commented 2 years ago

Today i had some times to dive into this and it seems i'm stuck in a loop. Let me explain:

I setup Entertainment with the code:

//Initialize streaming client
StreamingHueClient client = new StreamingHueClient(ip, key, entertainmentKey);

//Get the entertainment group
var all = await client.LocalHueClient.GetEntertainmentGroups();
var group = all.FirstOrDefault();

//Create a streaming group
var entGroup = new StreamingGroup(group.Locations);

//Connect to the streaming group
await client.Connect(group.Id);

//Start auto updating this entertainment group
client.AutoUpdate(entGroup, 50);

Then if you open the Hue app on your Phone you will see that streaming is active. So far so good.

In the background i have a Task that polls every X seconds to check if HueBridgeConfig.IsStreamingActive is still active. If not it will try an restore the streaming. For that i tried several options but let's start with this:

//Get the entertainment group
var all = await client.LocalHueClient.GetEntertainmentGroups();
var group = all.FirstOrDefault();

//Create a streaming group
var entGroup = new StreamingGroup(group.Locations);

//Connect to the streaming group
await client.Connect(group.Id);

//Start auto updating this entertainment group
client.AutoUpdate(entGroup, 50);

Now in the Hue app on your Phone click "STOP streaming", the poll task will kick in and fires above code. This results in Streaming Mode being activated again(Yeah!) but after a short time Streaming stops again and i don't know why. Resulting in the poll task kicking in and reconnect, etc.. etc..

I also tried only this single line, with the same result.

 var enableResult = await _localHueClient.SetStreamingAsync(groupId);

The only thing i haven't tried yet is also creating a new instance of New StreamingHueClient but it seams a bit unnecessary to re-active a stopped StreamingGroup...

Any tips?

EDIT 23-07-2021: Added complete test code:

Alright i created a new simple project just to dig down to see what is going wrong. Let me first start with the versions:

<?xml version="1.0" encoding="utf-8"?>
<packages>
  <package id="Newtonsoft.Json" version="13.0.1" targetFramework="net472" />
  <package id="Portable.BouncyCastle" version="1.8.10" targetFramework="net472" />
  <package id="Q42.HueApi" version="3.18.1" targetFramework="net472" />
  <package id="Q42.HueApi.ColorConverters" version="3.18.1" targetFramework="net472" />
  <package id="Q42.HueApi.Entertainment" version="3.18.1" targetFramework="net472" />
  <package id="System.IO" version="4.3.0" targetFramework="net472" />
  <package id="System.Net.Http" version="4.3.4" targetFramework="net472" />
  <package id="System.Runtime" version="4.3.0" targetFramework="net472" />
  <package id="System.Security.Cryptography.Algorithms" version="4.3.0" targetFramework="net472" />
  <package id="System.Security.Cryptography.Encoding" version="4.3.0" targetFramework="net472" />
  <package id="System.Security.Cryptography.Primitives" version="4.3.0" targetFramework="net472" />
  <package id="System.Security.Cryptography.X509Certificates" version="4.3.0" targetFramework="net472" />
</packages>

The project is a WPF app running .NET Framework 4.7.2

The Hue Bridge: ApiVersion = 1.45.0 DataStoreVersion = 107 ModelId = BSB002 SoftwareVersion = 1946080020

The test project is simple and only got one TestButton that initializes everything needed for only the use of EntertainmentAPI. Then it got a Task that act's like a watchdog. I simplyfied this and discoverd that only Client.Connect(Id) is needed for a restore.

Sorry but the code is in VB.NET but shoudn't be a problem:

Imports System.Threading
Imports Q42.HueApi
Imports Q42.HueApi.ColorConverters
Imports Q42.HueApi.Models.Bridge
Imports Q42.HueApi.Models.Groups
Imports Q42.HueApi.Streaming
Imports Q42.HueApi.Streaming.Extensions
Imports Q42.HueApi.Streaming.Models

Class MainWindow

    Private entLayer As EntertainmentLayer

    Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)

        'Register app with the bridge
        Dim streamClientKey As RegisterEntertainmentResult = Await New LocalHueClient("192.168.70.60").RegisterAsync("MyApp", "MyDevice", True)

        'Initialize streaming client
        Dim client As New StreamingHueClient(streamClientKey.Ip, streamClientKey.Username, streamClientKey.StreamingClientKey)

        'Get the entertainment group
        Dim all As IReadOnlyList(Of Group) = Await client.LocalHueClient.GetEntertainmentGroups()
        Dim group As Group = all.FirstOrDefault()

        'Create a streaming group
        Dim entGroup As New StreamingGroup(group.Locations)

        'Connect to the streaming group
        Await client.Connect(group.Id)

        'Start auto updating this entertainment group
        Dim SetAndForgetTask As Task = client.AutoUpdate(entGroup, CancellationToken.None, 50)

        'Create new base layer
        entLayer = entGroup.GetNewLayer(True)
        entLayer.SetBrightness(CancellationToken.None, 1)

        'Setup Watchdog for streaming with auto recovery
        Await Task.Run(Async Function()

                           Do While True

                               Dim LocalHueBridge As Bridge = Await client.LocalHueClient.GetBridgeAsync

                               If LocalHueBridge.IsStreamingActive Then
                                   Console.WriteLine("Streaming is still active....")

                                   'Loop through RGB to show it works
                                   entLayer.SetColor(CancellationToken.None, New RGBColor("FF0000")) 'Set Red
                                   Await Task.Delay(1500)
                                   entLayer.SetColor(CancellationToken.None, New RGBColor("00FF00")) 'Set Green
                                   Await Task.Delay(1500)
                                   entLayer.SetColor(CancellationToken.None, New RGBColor("0000FF")) 'Set Blue

                               Else
                                   Console.WriteLine("Streaming stopped, Re-enable Streaming ...")

                                    'Connect to the streaming group
                                    Await client.Connect(group.Id)
                               End If

                               Await Task.Delay(5000)
                           Loop

                           Return True
                       End Function)
    End Sub
End Class

Alright now this works and also restores the Streaming Mode. BUT.... there is a catch... If you open the Hue app and click on STOP streaming than the watchdog will restore it. You will see in the app that StreamingMode is restore, but after a few second it is stopped. And that's the point where you will get an exception on Client.Connect(Id). Which is:

Org.BouncyCastle.Crypto.Tls.TlsFatalAlert
  HResult=0x80131620
  Message=internal_error(80)
  Source=BouncyCastle.Crypto
  StackTrace:
   at Org.BouncyCastle.Crypto.Tls.DtlsClientProtocol.Connect(TlsClient client, DatagramTransport transport) in /_/crypto/src/crypto/tls/DtlsClientProtocol.cs:line 68
   at Q42.HueApi.Streaming.StreamingHueClient.<Connect>d__12.MoveNext()
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.GetResult()
   at Q42HueTest.MainWindow._Closure$__2-0.VB$StateMachine___Lambda$__0.MoveNext() in L:\VisualStudioProjects\Experimenteer\Q42HueTest\Q42HueTest\MainWindow.xaml.vb:line 58
   at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Q42HueTest.MainWindow.VB$StateMachine_2_Button_Click.MoveNext() in L:\VisualStudioProjects\Experimenteer\Q42HueTest\Q42HueTest\MainWindow.xaml.vb:line 40

  This exception was originally thrown at this call stack:
    [External Code]

Inner Exception 1:
TimeoutException: The operation has timed out.

This could mean two things:

  1. This is a TimeOut from the initial Client.Connect(Id) (but i don't think so)
  2. The BouncyCastle is stuck between states, thinking it isn't connected anymore while it is.

Or maybe something else??

TripleNico commented 2 years ago

It seems that i have got this working now, and this is the code for it:

Class MainWindow

    Private entLayer As EntertainmentLayer

    Private Async Sub Button_Click(sender As Object, e As RoutedEventArgs)

        'Register app with the bridge
        Dim streamClientKey As RegisterEntertainmentResult = Await New LocalHueClient("192.168.70.60").RegisterAsync("MyApp", "MyDevice", True)

        'Initialize streaming client
        Dim client As New StreamingHueClient(streamClientKey.Ip, streamClientKey.Username, streamClientKey.StreamingClientKey)

        'Get the entertainment group
        Dim all As IReadOnlyList(Of Group) = Await client.LocalHueClient.GetEntertainmentGroups()
        Dim group As Group = all.FirstOrDefault()

        'Create a streaming group
        Dim entGroup As New StreamingGroup(group.Locations)

        'Connect to the streaming group
        Await client.Connect(group.Id)

        'Start auto updating this entertainment group
        Dim SetAndForgetTask As Task = client.AutoUpdate(entGroup, CancellationToken.None, 50)

        'Create new base layer
        entLayer = entGroup.GetNewLayer(True)
        entLayer.SetBrightness(CancellationToken.None, 1)

        'Setup Watchdog for streaming with auto recovery
        Await Task.Run(Async Function()

                           Do While True

                               Dim LocalHueBridge As Bridge = Await client.LocalHueClient.GetBridgeAsync

                               If LocalHueBridge.IsStreamingActive Then
                                   Console.WriteLine("Streaming is still active....")

                                   'Loop through RGB to show it works
                                   entLayer.SetColor(CancellationToken.None, New RGBColor("FF0000")) 'Set Red
                                   Await Task.Delay(500)
                                   entLayer.SetColor(CancellationToken.None, New RGBColor("00FF00")) 'Set Green
                                   Await Task.Delay(500)
                                   entLayer.SetColor(CancellationToken.None, New RGBColor("0000FF")) 'Set Blue

                               Else
                                   Console.WriteLine("Streaming stopped, Re-enable Streaming ...")

                                   'First we have to close the current socket and setup a new one:
                                   client.Close()

                                   'Now create new StreamingClient
                                   client = New StreamingHueClient(streamClientKey.Ip, streamClientKey.Username, streamClientKey.StreamingClientKey)

                                   'Connect to the streaming group
                                   Await client.Connect(group.Id)

                                   'Start auto updating this entertainment group
                                   SetAndForgetTask = client.AutoUpdate(entGroup, CancellationToken.None, 50)
                               End If

                               Await Task.Delay(2500)
                           Loop

                           Return True
                       End Function)
    End Sub
End Class