From 088af3c1e4aebf7bed79a880e8c001343a8f26d5 Mon Sep 17 00:00:00 2001 From: Romain ODDONE Date: Mon, 27 Jul 2020 17:10:55 +0200 Subject: [PATCH 1/6] start music mode dev by creating a new TCP listener to replace the current TCPClient --- YeelightAPI/Device.IDeviceController.cs | 20 +++++++++++-- YeelightAPI/Device.cs | 40 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/YeelightAPI/Device.IDeviceController.cs b/YeelightAPI/Device.IDeviceController.cs index 0565e09..1b8e885 100644 --- a/YeelightAPI/Device.IDeviceController.cs +++ b/YeelightAPI/Device.IDeviceController.cs @@ -343,7 +343,15 @@ public async Task StartMusicMode(string hostName, int port) method: METHODS.SetMusicMode, parameters: parameters); - return result.IsOk(); + if (result.IsOk()) + { + //enables the music mode + await this.InitMusicModeAsync(hostName, port); + + return true; + } + + return false; } /// @@ -370,7 +378,15 @@ public async Task StopMusicMode() method: METHODS.SetMusicMode, parameters: parameters); - return result.IsOk(); + if (result.IsOk()) + { + //disables the music mode + this.IsMusicModeEnabled = false; + await DisableMusicModeAsync(); + return true; + } + + return false; } /// diff --git a/YeelightAPI/Device.cs b/YeelightAPI/Device.cs index ecd7e4c..04687b8 100644 --- a/YeelightAPI/Device.cs +++ b/YeelightAPI/Device.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.Net; using System.Net.Sockets; using System.Text; using System.Threading; @@ -90,6 +91,11 @@ public bool IsConnected } } + /// + /// Indicate wether the music mode is enabled + /// + public bool IsMusicModeEnabled { get; private set; } + /// /// The model. /// @@ -316,10 +322,44 @@ internal async Task> ExecuteCommandWithResponse(METHODS meth return null; } + internal async Task InitMusicModeAsync(string hostname, int port) + { + this.IsMusicModeEnabled = true; + //init new TCP socket + if (string.IsNullOrWhiteSpace(hostname)) + { + hostname = GetLocalIpAddress(); + } + + var listener = new TcpListener(IPAddress.Parse(hostname), port); + listener.Start(); + var musicTcpClient = await listener.AcceptTcpClientAsync(); + _tcpClient = musicTcpClient; + + } + + internal async Task DisableMusicModeAsync() + { + _tcpClient = null; + _ = await Connect(); + this.IsMusicModeEnabled = false; + + } + #endregion INTERNAL METHODS #region PRIVATE METHODS + private static string GetLocalIpAddress() + { + using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, 0)) + { + socket.Connect("8.8.8.8", 65530); + IPEndPoint endPoint = socket.LocalEndPoint as IPEndPoint; + return endPoint.Address.ToString(); + } + } + /// /// Generate valid parameters for percent values /// From 40f94fcf27f9e82e916ed6e0130c325a210fa3de Mon Sep 17 00:00:00 2001 From: romain oddone Date: Wed, 29 Jul 2020 14:45:53 +0200 Subject: [PATCH 2/6] handle the fact that no response is sent back from the device in music mode and cancel the watch task on device disconnection --- YeelightAPI/Device.IDeviceController.cs | 25 ++++++++++----- YeelightAPI/Device.cs | 41 ++++++++++++------------- YeelightAPI/Models/CommandResult.cs | 11 ++++++- 3 files changed, 48 insertions(+), 29 deletions(-) diff --git a/YeelightAPI/Device.IDeviceController.cs b/YeelightAPI/Device.IDeviceController.cs index 1b8e885..9dd8443 100644 --- a/YeelightAPI/Device.IDeviceController.cs +++ b/YeelightAPI/Device.IDeviceController.cs @@ -155,6 +155,8 @@ public void Disconnect() { _tcpClient.Close(); _tcpClient = null; + _watchCancellationTokenSource.Cancel(); + _watchCancellationTokenSource.Dispose(); } } @@ -335,9 +337,19 @@ public async Task StartColorFlow(ColorFlow flow) /// /// /// - public async Task StartMusicMode(string hostName, int port) + public async Task StartMusicMode(string hostname = null, int port = 12345) { - List parameters = new List() { (int)MusicAction.On, hostName, port }; + this.IsMusicModeEnabled = true; + //init new TCP socket + if (string.IsNullOrWhiteSpace(hostname)) + { + hostname = GetLocalIpAddress(); + } + + var listener = new TcpListener(System.Net.IPAddress.Parse(hostname), port); + listener.Start(); + + List parameters = new List() { (int)MusicAction.On, hostname, port }; CommandResult> result = await ExecuteCommandWithResponse>( method: METHODS.SetMusicMode, @@ -345,13 +357,12 @@ public async Task StartMusicMode(string hostName, int port) if (result.IsOk()) { - //enables the music mode - await this.InitMusicModeAsync(hostName, port); - - return true; + this.Disconnect(); + var musicTcpClient = await listener.AcceptTcpClientAsync(); + _tcpClient = musicTcpClient; } - return false; + return result.IsOk(); } /// diff --git a/YeelightAPI/Device.cs b/YeelightAPI/Device.cs index 04687b8..4cae9dd 100644 --- a/YeelightAPI/Device.cs +++ b/YeelightAPI/Device.cs @@ -38,6 +38,11 @@ public partial class Device : IDisposable /// private TcpClient _tcpClient; + /// + /// Cancellation token source for the Watch task + /// + private CancellationTokenSource _watchCancellationTokenSource; + #endregion PRIVATE ATTRIBUTES #region EVENTS @@ -257,6 +262,15 @@ public void ExecuteCommand(METHODS method, List parameters = null) /// public async Task> ExecuteCommandWithResponse(METHODS method, List parameters = null) { + if (IsMusicModeEnabled) + { + //music mode enabled, there will be no response, we should assume everything works + int uniqueId = GetUniqueIdForCommand(); + ExecuteCommand(method, uniqueId, parameters); + return new CommandResult() { Id = uniqueId, Error = null, IsMusicResponse = true }; + } + + //default behavior : send command and wait for response return await ExecuteCommandWithResponse(method, GetUniqueIdForCommand(), parameters); } @@ -267,7 +281,7 @@ public async Task> ExecuteCommandWithResponse(METHODS method /// public override string ToString() { - return $"{this.Model.ToString()} ({this.Hostname}:{this.Port})"; + return $"{Model.ToString()} ({Hostname}:{Port})"; } #endregion PUBLIC METHODS @@ -322,27 +336,10 @@ internal async Task> ExecuteCommandWithResponse(METHODS meth return null; } - internal async Task InitMusicModeAsync(string hostname, int port) - { - this.IsMusicModeEnabled = true; - //init new TCP socket - if (string.IsNullOrWhiteSpace(hostname)) - { - hostname = GetLocalIpAddress(); - } - - var listener = new TcpListener(IPAddress.Parse(hostname), port); - listener.Start(); - var musicTcpClient = await listener.AcceptTcpClientAsync(); - _tcpClient = musicTcpClient; - - } - internal async Task DisableMusicModeAsync() { - _tcpClient = null; _ = await Connect(); - this.IsMusicModeEnabled = false; + IsMusicModeEnabled = false; } @@ -470,10 +467,12 @@ private async Task> UnsafeExecuteCommandWithResponse(METHODS /// private async Task Watch() { + _watchCancellationTokenSource = new CancellationTokenSource(); + await Task.Factory.StartNew(async () => { //while device is connected - while (_tcpClient != null) + while (_tcpClient != null && _watchCancellationTokenSource.IsCancellationRequested == false) { lock (_syncLock) { @@ -562,7 +561,7 @@ await Task.Factory.StartNew(async () => await Task.Delay(100); } - }, TaskCreationOptions.LongRunning); + }, _watchCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } /// diff --git a/YeelightAPI/Models/CommandResult.cs b/YeelightAPI/Models/CommandResult.cs index 68ac8da..7ba55f8 100644 --- a/YeelightAPI/Models/CommandResult.cs +++ b/YeelightAPI/Models/CommandResult.cs @@ -12,11 +12,14 @@ public static class CommandResultExtensions /// /// Determine if the result is a classical OK result ({"id":1, "result":["ok"]}) /// + /// + /// returns true by default if music mode is enabled + /// /// /// public static bool IsOk(this CommandResult> @this) { - return @this?.Error == null && @this?.Result?[0] == "ok"; + return @this?.IsMusicResponse == true || (@this?.Error == null && @this?.Result?[0] == "ok"); } #endregion Public Methods @@ -41,6 +44,12 @@ public class CommandResult #endregion Public Properties + #region Internal Properties + + internal bool IsMusicResponse { get; set; } + + #endregion Internal Properties + #region Public Classes /// From 610884628ca51e5ec9b73b5907bc299b21c6c18c Mon Sep 17 00:00:00 2001 From: romain oddone Date: Wed, 29 Jul 2020 14:46:41 +0200 Subject: [PATCH 3/6] fix parameter typo --- YeelightAPI/Device.IDeviceController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/YeelightAPI/Device.IDeviceController.cs b/YeelightAPI/Device.IDeviceController.cs index 9dd8443..642881e 100644 --- a/YeelightAPI/Device.IDeviceController.cs +++ b/YeelightAPI/Device.IDeviceController.cs @@ -334,7 +334,7 @@ public async Task StartColorFlow(ColorFlow flow) /// /// Starts the music mode /// - /// + /// /// /// public async Task StartMusicMode(string hostname = null, int port = 12345) From 15b822d1388a3e87a0d2ea9791c95f081d230af9 Mon Sep 17 00:00:00 2001 From: romain oddone Date: Tue, 4 Aug 2020 11:44:52 +0200 Subject: [PATCH 4/6] Fix ObjectDisposedException when Stopping music mode --- YeelightAPI/Device.IDeviceController.cs | 7 +- YeelightAPI/Device.cs | 129 ++++++++++++------------ 2 files changed, 70 insertions(+), 66 deletions(-) diff --git a/YeelightAPI/Device.IDeviceController.cs b/YeelightAPI/Device.IDeviceController.cs index 642881e..03f4bcb 100644 --- a/YeelightAPI/Device.IDeviceController.cs +++ b/YeelightAPI/Device.IDeviceController.cs @@ -155,8 +155,11 @@ public void Disconnect() { _tcpClient.Close(); _tcpClient = null; - _watchCancellationTokenSource.Cancel(); - _watchCancellationTokenSource.Dispose(); + try + { + _watchCancellationTokenSource.Cancel(); + } + catch (ObjectDisposedException) { } } } diff --git a/YeelightAPI/Device.cs b/YeelightAPI/Device.cs index 4cae9dd..64dcce2 100644 --- a/YeelightAPI/Device.cs +++ b/YeelightAPI/Device.cs @@ -467,101 +467,102 @@ private async Task> UnsafeExecuteCommandWithResponse(METHODS /// private async Task Watch() { - _watchCancellationTokenSource = new CancellationTokenSource(); - - await Task.Factory.StartNew(async () => + using (_watchCancellationTokenSource = new CancellationTokenSource()) { - //while device is connected - while (_tcpClient != null && _watchCancellationTokenSource.IsCancellationRequested == false) + await Task.Run(async () => { - lock (_syncLock) + //while device is connected + while (_tcpClient != null && _watchCancellationTokenSource.IsCancellationRequested == false) { - if (_tcpClient != null) + lock (_syncLock) { - //automatic re-connection - if (!_tcpClient.IsConnected()) + if (_tcpClient != null) { - _tcpClient.ConnectAsync(Hostname, Port).Wait(); - } + //automatic re-connection + if (!_tcpClient.IsConnected()) + { + _tcpClient.ConnectAsync(Hostname, Port).Wait(); + } - if (_tcpClient.IsConnected()) - { - //there is data avaiblable in the pipe - if (_tcpClient.Client.Available > 0) + if (_tcpClient.IsConnected()) { - byte[] bytes = new byte[_tcpClient.Client.Available]; + //there is data avaiblable in the pipe + if (_tcpClient.Client.Available > 0) + { + byte[] bytes = new byte[_tcpClient.Client.Available]; - //read datas - _tcpClient.Client.Receive(bytes); + //read datas + _tcpClient.Client.Receive(bytes); - try - { - string datas = Encoding.UTF8.GetString(bytes); - if (!string.IsNullOrEmpty(datas)) + try { - //get every messages in the pipe - foreach (string entry in datas.Split(new string[] { Constants.LineSeparator }, - StringSplitOptions.RemoveEmptyEntries)) + string datas = Encoding.UTF8.GetString(bytes); + if (!string.IsNullOrEmpty(datas)) { - CommandResult commandResult = - JsonConvert.DeserializeObject(entry, Constants.DeviceSerializerSettings); - if (commandResult != null && commandResult.Id != 0) + //get every messages in the pipe + foreach (string entry in datas.Split(new string[] { Constants.LineSeparator }, + StringSplitOptions.RemoveEmptyEntries)) { - ICommandResultHandler commandResultHandler; - lock (_currentCommandResults) + CommandResult commandResult = + JsonConvert.DeserializeObject(entry, Constants.DeviceSerializerSettings); + if (commandResult != null && commandResult.Id != 0) { - if (!_currentCommandResults.TryGetValue(commandResult.Id, out commandResultHandler)) - continue; // ignore if the result can't be found - } + ICommandResultHandler commandResultHandler; + lock (_currentCommandResults) + { + if (!_currentCommandResults.TryGetValue(commandResult.Id, out commandResultHandler)) + continue; // ignore if the result can't be found + } - if (commandResult.Error == null) - { - commandResult = (CommandResult)JsonConvert.DeserializeObject(entry, commandResultHandler.ResultType, Constants.DeviceSerializerSettings); - commandResultHandler.SetResult(commandResult); + if (commandResult.Error == null) + { + commandResult = (CommandResult)JsonConvert.DeserializeObject(entry, commandResultHandler.ResultType, Constants.DeviceSerializerSettings); + commandResultHandler.SetResult(commandResult); + } + else + { + commandResultHandler.SetError(commandResult.Error); + } } else { - commandResultHandler.SetError(commandResult.Error); - } - } - else - { - NotificationResult notificationResult = - JsonConvert.DeserializeObject(entry, - Constants.DeviceSerializerSettings); + NotificationResult notificationResult = + JsonConvert.DeserializeObject(entry, + Constants.DeviceSerializerSettings); - if (notificationResult != null && notificationResult.Method != null) - { - if (notificationResult.Params != null) + if (notificationResult != null && notificationResult.Method != null) { - //save properties - foreach (KeyValuePair property in - notificationResult.Params) + if (notificationResult.Params != null) { - this[property.Key] = property.Value; + //save properties + foreach (KeyValuePair property in + notificationResult.Params) + { + this[property.Key] = property.Value; + } } - } - //notification result - OnNotificationReceived?.Invoke(this, - new NotificationReceivedEventArgs(notificationResult)); + //notification result + OnNotificationReceived?.Invoke(this, + new NotificationReceivedEventArgs(notificationResult)); + } } } } } - } - catch (Exception ex) - { - OnError?.Invoke(this, new UnhandledExceptionEventArgs(ex, false)); + catch (Exception ex) + { + OnError?.Invoke(this, new UnhandledExceptionEventArgs(ex, false)); + } } } } } - } - await Task.Delay(100); - } - }, _watchCancellationTokenSource.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + await Task.Delay(100); + } + }, _watchCancellationTokenSource.Token); + } } /// From 5bb4125eb14416011b896bcc1f5ba2f0a07a38fa Mon Sep 17 00:00:00 2001 From: romain oddone Date: Fri, 7 Aug 2020 16:04:06 +0200 Subject: [PATCH 5/6] add ToString override to DeviceGroup --- YeelightAPI/DeviceGroup.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/YeelightAPI/DeviceGroup.cs b/YeelightAPI/DeviceGroup.cs index 1a6b47b..a63e40b 100644 --- a/YeelightAPI/DeviceGroup.cs +++ b/YeelightAPI/DeviceGroup.cs @@ -104,6 +104,11 @@ protected async Task Process(Func> f) return result; } + public override string ToString() + { + return $"{this.Name} ({this.Count} devices)"; + } + #endregion Protected Methods } } \ No newline at end of file From 29276c4dc3148d0a29e0d619e9bc01ecd26ed5d1 Mon Sep 17 00:00:00 2001 From: romain oddone Date: Fri, 7 Aug 2020 16:16:48 +0200 Subject: [PATCH 6/6] correctly implement IDisposable (thx @BionicCode) --- YeelightAPI/Device.cs | 49 ++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/YeelightAPI/Device.cs b/YeelightAPI/Device.cs index 64dcce2..bbc84b7 100644 --- a/YeelightAPI/Device.cs +++ b/YeelightAPI/Device.cs @@ -227,21 +227,6 @@ public object this[string propertyName] #region PUBLIC METHODS - #region IDisposable - - /// - /// Dispose the device - /// - public void Dispose() - { - lock (_syncLock) - { - Disconnect(); - } - } - - #endregion IDisposable - /// /// Execute a command /// @@ -575,5 +560,39 @@ private int GetUniqueIdForCommand() } #endregion PRIVATE METHODS + + #region IDisposable + + private void ReleaseUnmanagedResources() + { + // TODO release unmanaged resources here + } + + private void Dispose(bool disposing) + { + ReleaseUnmanagedResources(); + if (disposing) + { + lock (_syncLock) + { + Disconnect(); + } + } + } + + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + ~Device() + { + Dispose(false); + } + + #endregion IDisposable } } \ No newline at end of file