From 372b4331b4871ad1b2c26aa569793d89c9a97220 Mon Sep 17 00:00:00 2001 From: Alexey 'Cluster' Avdyukhin Date: Tue, 5 Jul 2022 20:48:22 +0400 Subject: Bugfixes, custom timeout parameter, cancellation tokens support. --- TuyaApi.cs | 23 ++++---- TuyaDevice.cs | 174 +++++++++++++++++++++------------------------------------- 2 files changed, 75 insertions(+), 122 deletions(-) diff --git a/TuyaApi.cs b/TuyaApi.cs index fdebac6..a3e4c83 100644 --- a/TuyaApi.cs +++ b/TuyaApi.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net.Http; using System.Security.Cryptography; using System.Text; +using System.Threading; using System.Threading.Tasks; namespace com.clusterrr.TuyaNet @@ -113,7 +114,7 @@ namespace com.clusterrr.TuyaNet /// Execute query without token. /// Refresh access token even it's not expired. /// JSON string with response. - public async Task RequestAsync(Method method, string uri, string body = null, Dictionary headers = null, bool noToken = false, bool forceTokenRefresh = false) + public async Task RequestAsync(Method method, string uri, string body = null, Dictionary headers = null, bool noToken = false, bool forceTokenRefresh = false, CancellationToken cancellationToken = default) { while (uri.StartsWith("/")) uri = uri.Substring(1); var urlHost = RegionToHost(region); @@ -138,7 +139,7 @@ namespace com.clusterrr.TuyaNet } else { - await RefreshAccessTokenAsync(forceTokenRefresh); + await RefreshAccessTokenAsync(forceTokenRefresh, cancellationToken); payload += token.AccessToken + now; } @@ -169,7 +170,7 @@ namespace com.clusterrr.TuyaNet { Method.GET => HttpMethod.Get, Method.POST => HttpMethod.Post, - Method.PUT => HttpMethod.Delete, + Method.PUT => HttpMethod.Put, Method.DELETE => HttpMethod.Delete, _ => throw new NotSupportedException($"Unknow method - {method}") }, @@ -180,7 +181,7 @@ namespace com.clusterrr.TuyaNet if (body != null) httpRequestMessage.Content = new StringContent(body, Encoding.UTF8, "application/json"); - using (var response = await httpClient.SendAsync(httpRequestMessage).ConfigureAwait(false)) + using (var response = await httpClient.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false)) { var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(false); var root = JObject.Parse(responseString); @@ -194,14 +195,14 @@ namespace com.clusterrr.TuyaNet /// /// Request access token if it's expired or not requested yet. /// - private async Task RefreshAccessTokenAsync(bool force = false) + private async Task RefreshAccessTokenAsync(bool force = false, CancellationToken cancellationToken = default) { if (force || (token == null) || (tokenTime.AddSeconds(token.ExpireTime) >= DateTime.Now) // For some weird reason token expires sooner than it should || (tokenTime.AddMinutes(30) >= DateTime.Now)) { var uri = "v1.0/token?grant_type=1"; - var response = await RequestAsync(Method.GET, uri, noToken: true); + var response = await RequestAsync(Method.GET, uri, noToken: true, cancellationToken: cancellationToken); token = JsonConvert.DeserializeObject(response); tokenTime = DateTime.Now; } @@ -213,10 +214,10 @@ namespace com.clusterrr.TuyaNet /// Device ID. /// Refresh access token even it's not expired. /// Device info. - public async Task GetDeviceInfoAsync(string deviceId, bool forceTokenRefresh = false) + public async Task GetDeviceInfoAsync(string deviceId, bool forceTokenRefresh = false, CancellationToken cancellationToken = default) { var uri = $"v1.0/devices/{deviceId}"; - var response = await RequestAsync(Method.GET, uri, forceTokenRefresh: forceTokenRefresh); + var response = await RequestAsync(Method.GET, uri, forceTokenRefresh: forceTokenRefresh, cancellationToken: cancellationToken); var device = JsonConvert.DeserializeObject(response); return device; } @@ -227,11 +228,11 @@ namespace com.clusterrr.TuyaNet /// ID of any registered device. /// Refresh access token even it's not expired. /// Array of devices info. - public async Task GetAllDevicesInfoAsync(string anyDeviceId, bool forceTokenRefresh = false) + public async Task GetAllDevicesInfoAsync(string anyDeviceId, bool forceTokenRefresh = false, CancellationToken cancellationToken = default) { - var userId = (await GetDeviceInfoAsync(anyDeviceId, forceTokenRefresh: forceTokenRefresh)).UserId; + var userId = (await GetDeviceInfoAsync(anyDeviceId, forceTokenRefresh: forceTokenRefresh, cancellationToken: cancellationToken)).UserId; var uri = $"v1.0/users/{userId}/devices"; - var response = await RequestAsync(Method.GET, uri, forceTokenRefresh: false); // Token already refreshed + var response = await RequestAsync(Method.GET, uri, forceTokenRefresh: false, cancellationToken: cancellationToken); // Token already refreshed var devices = JsonConvert.DeserializeObject(response); return devices; } diff --git a/TuyaDevice.cs b/TuyaDevice.cs index f5e0b78..30f764c 100644 --- a/TuyaDevice.cs +++ b/TuyaDevice.cs @@ -24,7 +24,7 @@ namespace com.clusterrr.TuyaNet /// Device ID. /// Protocol version. /// TCP port of device. - /// Receive timeout. + /// Receive timeout (msec). public TuyaDevice(string ip, string localKey, string deviceId, TuyaProtocolVersion protocolVersion = TuyaProtocolVersion.V33, int port = 6668, int receiveTimeout = 250) { IP = ip; @@ -75,6 +75,14 @@ namespace com.clusterrr.TuyaNet /// public int ReceiveTimeout { get; set; } /// + /// Network error retry interval (msec) + /// + public int NetworkErrorRetriesInterval { get; set; } = 100; + /// + /// Empty responce retry interval (msec) + /// + public int NullRetriesInterval { get; set; } = 0; + /// /// Permanent connection (connect and stay connected). /// public bool PermanentConnection { get; set; } = false; @@ -139,20 +147,22 @@ namespace com.clusterrr.TuyaNet /// /// Tuya command ID. /// JSON string. - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. + /// Number of retries in case of network error (default - 2). + /// Number of retries in case of empty answer (default - 1). + /// Override receive timeout (default - ReceiveTimeout property). /// Parsed and decrypred received data as instance of TuyaLocalResponse. - public async Task SendAsync(TuyaCommand command, string json, int retries = 2, int nullRetries = 1) - => DecodeResponse(await SendAsync(EncodeRequest(command, json), retries, nullRetries)); + public async Task SendAsync(TuyaCommand command, string json, int retries = 2, int nullRetries = 1, int? overrideRecvTimeout = null, CancellationToken cancellationToken = default) + => DecodeResponse(await SendAsync(EncodeRequest(command, json), retries, nullRetries, overrideRecvTimeout, cancellationToken)); /// /// Sends raw data over to device and read response. /// /// Raw data to send. - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. + /// Number of retries in case of network error (default - 2). + /// Number of retries in case of empty answer (default - 1). + /// Override receive timeout (default - ReceiveTimeout property). /// Received data (raw). - public async Task SendAsync(byte[] data, int retries = 2, int nullRetries = 1) + public async Task SendAsync(byte[] data, int retries = 2, int nullRetries = 1, int? overrideRecvTimeout = null, CancellationToken cancellationToken = default) { Exception lastException = null; while (retries-- > 0) @@ -168,8 +178,8 @@ namespace com.clusterrr.TuyaNet if (client == null) client = new TcpClient(IP, Port); var stream = client.GetStream(); - await stream.WriteAsync(data, 0, data.Length).ConfigureAwait(false); - return await ReceiveAsync(stream, nullRetries); + await stream.WriteAsync(data, 0, data.Length, cancellationToken).ConfigureAwait(false); + return await ReceiveAsync(stream, nullRetries, overrideRecvTimeout, cancellationToken); } catch (Exception ex) when (ex is IOException or TimeoutException) { @@ -177,6 +187,10 @@ namespace com.clusterrr.TuyaNet // retry at least once lastException = ex; } + catch (Exception ex) + { + throw ex; + } finally { if (!PermanentConnection || (client?.Connected == false) || (lastException != null)) @@ -186,12 +200,12 @@ namespace com.clusterrr.TuyaNet client = null; } } - await Task.Delay(500); + await Task.Delay(NetworkErrorRetriesInterval, cancellationToken); } throw lastException; } - private async Task ReceiveAsync(NetworkStream stream, int nullRetries = 1) + private async Task ReceiveAsync(NetworkStream stream, int nullRetries = 1, int? overrideRecvTimeout = null, CancellationToken cancellationToken = default) { byte[] result; byte[] buffer = new byte[1024]; @@ -200,16 +214,16 @@ namespace com.clusterrr.TuyaNet int length = buffer.Length; while ((ms.Length < 16) || ((length = BitConverter.ToInt32(TuyaParser.BigEndian(ms.ToArray().Skip(12).Take(4)).ToArray(), 0) + 16) < ms.Length)) { - var cancellationTokenSource = new CancellationTokenSource(); - var readTask = stream.ReadAsync(buffer, 0, length, cancellationToken: cancellationTokenSource.Token); - var timeoutTask = Task.Delay(ReceiveTimeout, cancellationToken: cancellationTokenSource.Token); + var timeoutCancellationTokenSource = new CancellationTokenSource(); + var readTask = stream.ReadAsync(buffer, 0, length, cancellationToken: cancellationToken); + var timeoutTask = Task.Delay(overrideRecvTimeout ?? ReceiveTimeout, cancellationToken: timeoutCancellationTokenSource.Token); var t = await Task.WhenAny(readTask, timeoutTask).ConfigureAwait(false); - cancellationTokenSource.Cancel(); + timeoutCancellationTokenSource.Cancel(); int bytes = 0; if (t == timeoutTask) { if (stream.DataAvailable) - bytes = await stream.ReadAsync(buffer, 0, length); + bytes = await stream.ReadAsync(buffer, 0, length, cancellationToken); else throw new TimeoutException(); } @@ -225,7 +239,8 @@ namespace com.clusterrr.TuyaNet { try { - result = await ReceiveAsync(stream, nullRetries - 1); + await Task.Delay(NullRetriesInterval, cancellationToken); + result = await ReceiveAsync(stream, nullRetries - 1, overrideRecvTimeout: overrideRecvTimeout, cancellationToken); } catch { } } @@ -235,23 +250,14 @@ namespace com.clusterrr.TuyaNet /// /// Requests current DPs status. /// - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. - /// Dictionary of DP numbers and values. - [Obsolete("Use GetDpsAsync")] - public async Task> GetDps(int retries = 5, int nullRetries = 1) - => await GetDpsAsync(retries, nullRetries); - - /// - /// Requests current DPs status. - /// - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. + /// Number of retries in case of network error (default - 2). + /// Number of retries in case of empty answer (default - 1). + /// Override receive timeout (default - ReceiveTimeout property). /// Dictionary of DP numbers and values. - public async Task> GetDpsAsync(int retries = 5, int nullRetries = 1) + public async Task> GetDpsAsync(int retries = 5, int nullRetries = 1, int? overrideRecvTimeout = null, CancellationToken cancellationToken = default) { var requestJson = FillJson(null); - var response = await SendAsync(TuyaCommand.DP_QUERY, requestJson, retries, nullRetries); + var response = await SendAsync(TuyaCommand.DP_QUERY, requestJson, retries, nullRetries, overrideRecvTimeout, cancellationToken); if (string.IsNullOrEmpty(response.JSON)) throw new InvalidDataException("Response is empty"); var root = JObject.Parse(response.JSON); @@ -264,56 +270,24 @@ namespace com.clusterrr.TuyaNet /// /// DP number. /// Value. - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. + /// Number of retries in case of network error (default - 2). + /// Number of retries in case of empty answer (default - 1). + /// Override receive timeout (default - ReceiveTimeout property). + /// Do not throw exception on empty Response /// Dictionary of DP numbers and values. - [Obsolete("Use SetDpAsync")] - public async Task> SetDps(int dp, object value, int retries = 2, int nullRetries = 1) - => await SetDpsAsync(new Dictionary { { dp, value } }, retries, nullRetries); - - /// - /// Sets single DP to specified value. - /// - /// DP number. - /// Value. - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. - /// Dictionary of DP numbers and values. - [Obsolete("Use SetDpAsync")] - public async Task> SetDp(int dp, object value, int retries = 2, int nullRetries = 1) - => await SetDpAsync(dp, value, retries, nullRetries); - - /// - /// Sets single DP to specified value. - /// - /// DP number. - /// Value. - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. - /// Dictionary of DP numbers and values. - public async Task> SetDpAsync(int dp, object value, int retries = 2, int nullRetries = 1) - => await SetDpsAsync(new Dictionary { { dp, value } }, retries, nullRetries); + public async Task> SetDpAsync(int dp, object value, int retries = 2, int nullRetries = 1, int? overrideRecvTimeout = null, bool allowEmptyResponse = false, CancellationToken cancellationToken = default) + => await SetDpsAsync(new Dictionary { { dp, value } }, retries, nullRetries, overrideRecvTimeout, allowEmptyResponse, cancellationToken); /// /// Sets DPs to specified value. /// /// Dictionary of DP numbers and values to set. - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. + /// Number of retries in case of network error (default - 2). + /// Number of retries in case of empty answer (default - 1). + /// Override receive timeout (default - ReceiveTimeout property). + /// Do not throw exception on empty Response /// Dictionary of DP numbers and values. - [Obsolete("Use SetDpsAsync")] - public async Task> SetDps(Dictionary dps, int retries = 2, int nullRetries = 1) - => await SetDpsAsync(dps, retries, nullRetries); - - - /// - /// Sets DPs to specified value. - /// - /// Dictionary of DP numbers and values to set. - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. - /// Dictionary of DP numbers and values. - public async Task> SetDpsAsync(Dictionary dps, int retries = 2, int nullRetries = 1) + public async Task> SetDpsAsync(Dictionary dps, int retries = 2, int nullRetries = 1, int? overrideRecvTimeout = null, bool allowEmptyResponse = false, CancellationToken cancellationToken = default) { var cmd = new Dictionary { @@ -321,9 +295,14 @@ namespace com.clusterrr.TuyaNet }; string requestJson = JsonConvert.SerializeObject(cmd); requestJson = FillJson(requestJson); - var response = await SendAsync(TuyaCommand.CONTROL, requestJson, retries, nullRetries); + var response = await SendAsync(TuyaCommand.CONTROL, requestJson, retries, nullRetries, overrideRecvTimeout, cancellationToken); if (string.IsNullOrEmpty(response.JSON)) - throw new InvalidDataException("Response is empty"); + { + if (!allowEmptyResponse) + throw new InvalidDataException("Response is empty"); + else + return null; + } var root = JObject.Parse(response.JSON); var newDps = JsonConvert.DeserializeObject>(root.GetValue("dps").ToString()); return newDps.ToDictionary(kv => int.Parse(kv.Key), kv => kv.Value); @@ -333,38 +312,11 @@ namespace com.clusterrr.TuyaNet /// Update DP values. /// /// DP identificators to update (can be empty for some devices). + /// Number of retries in case of network error (default - 2). + /// Number of retries in case of empty answer (default - 1). + /// Override receive timeout (default - ReceiveTimeout property). /// Dictionary of DP numbers and values. - [Obsolete("Use UpdateDpsAsync")] - public async Task> UpdateDps(params int[] dpIds) - => await UpdateDpsAsync(dpIds, retries: 5, nullRetries: 1); - - /// - /// Update DP values. - /// - /// DP identificators to update (can be empty for some devices). - /// Dictionary of DP numbers and values. - public async Task> UpdateDpsAsync(params int[] dpIds) - => await UpdateDpsAsync(dpIds, retries: 5, nullRetries: 1); - - /// - /// Update DP values. - /// - /// DP identificators to update (can be empty for some devices). - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. - /// Dictionary of DP numbers and values. - [Obsolete("Use UpdateDpsAsync")] - public async Task> UpdateDps(IEnumerable dpIds, int retries = 5, int nullRetries = 1) - => await UpdateDpsAsync(dpIds, retries, nullRetries); - - /// - /// Update DP values. - /// - /// DP identificators to update (can be empty for some devices). - /// Number of retries in case of network error. - /// Number of retries in case of empty answer. - /// Dictionary of DP numbers and values. - public async Task> UpdateDpsAsync(IEnumerable dpIds, int retries = 5, int nullRetries = 1) + public async Task> UpdateDpsAsync(IEnumerable dpIds, int retries = 5, int nullRetries = 1, int? overrideRecvTimeout = null, CancellationToken cancellationToken = default) { var cmd = new Dictionary { @@ -372,7 +324,7 @@ namespace com.clusterrr.TuyaNet }; string requestJson = JsonConvert.SerializeObject(cmd); requestJson = FillJson(requestJson); - var response = await SendAsync(TuyaCommand.UPDATE_DPS, requestJson, retries, nullRetries); + var response = await SendAsync(TuyaCommand.UPDATE_DPS, requestJson, retries, nullRetries, overrideRecvTimeout, cancellationToken); if (string.IsNullOrEmpty(response.JSON)) return new Dictionary(); var root = JObject.Parse(response.JSON); @@ -384,12 +336,12 @@ namespace com.clusterrr.TuyaNet /// Get current local key from Tuya Cloud API /// /// Refresh access token even it's not expired. - public async Task RefreshLocalKeyAsync(bool forceTokenRefresh = false) + public async Task RefreshLocalKeyAsync(bool forceTokenRefresh = false, CancellationToken cancellationToken = default) { if (string.IsNullOrEmpty(accessId)) throw new ArgumentException("Access ID is not specified", "accessId"); if (string.IsNullOrEmpty(apiSecret)) throw new ArgumentException("API secret is not specified", "apiSecret"); var api = new TuyaApi(region, accessId, apiSecret); - var deviceInfo = await api.GetDeviceInfoAsync(DeviceId, forceTokenRefresh: forceTokenRefresh); + var deviceInfo = await api.GetDeviceInfoAsync(DeviceId, forceTokenRefresh: forceTokenRefresh, cancellationToken); LocalKey = deviceInfo.LocalKey; } -- cgit v1.2.3