Releases: oznetmaster/KasaTapoClient
Release list
v1.2.0
Migrated the JSON serialization stack from System.Text.Json to Newtonsoft.Json (13.0.3) across the core library, KasaClient.Console, tests, and benchmarks. This avoids extra binding-redirect dependencies that System.Text.Json requires on .NET Framework 4.7.2 and improves reliability on embedded Mono hosts. No public API or behavior changes; validated with a full solution build across net472/net10.0 and live device integration tests against real plugs, bulbs, light strips, and hubs. See CHANGELOG.md for details.
v1.1.11
Fixed a discovery result bug where a device that replied to both the legacy and smart/Tapo discovery broadcasts would only have one of the two results kept; both are now kept, keyed by host and transport kind. Added Discover.DiscoverLegacyAsync/DiscoveryClient.DiscoverLegacyAsync for broadcasting only the legacy (port 9999) discovery request. Fixed LegacyTransport to transparently reconnect and retry once when a reused idle connection was closed by the device, and to reliably drop the connection on a write/read timeout on .NET Framework. Fixed TpapTransport to disable system proxy auto-detection (which could stall the first connect for up to the full startup timeout on embedded Mono/Linux hosts) and raised its per-host connection limit to avoid connection-pool starvation after repeated timeout/abort cycles.
Full Changelog: v1.1.10...v1.1.11
v1.1.10
Fixed a startup performance issue in the TPAP transport where the first secure handshake per process could take several minutes on slower/embedded CPU hosts. The internal SecureRandom instance previously relied on BouncyCastle's default timing-based entropy seeding, which is CPU-speed dependent; it now seeds from the platform's cryptographically secure RNG (CryptoApiRandomGenerator) instead, eliminating the delay with no change to cryptographic security. This is an internal-only change with no public API impact. Added unit tests verifying fast, non-degenerate SecureRandom seeding.
Full Changelog: v1.1.9...v1.1.10
v1.1.9
Fix TpapTransport still absorbing an internal per-request timeout after the 1.1.8 fix: ShouldRetryLiveSession no longer treats TaskCanceledException/OperationCanceledException as retryable, but it still treated IOException/HttpRequestException messages containing "operation has been aborted" as retryable so genuine transport resets could recover. However, when SendOnceAsync's internal per-request timeout (CreateOperationTimeoutSource, separate from the caller's outer CancellationToken) elapses, SocketsHttpHandler aborts the socket and throws that exact same IOException/HttpRequestException shape rather than an OperationCanceledException, so the internal timeout was still misclassified as a retryable transport reset -- triggering Reset() plus a full, non-cancellable PAKE handshake instead of propagating the timeout. SendOnceAsync, PostLoginAsync, and SendKeepAliveCoreAsync now detect when the internal operation token (not the caller's outer token) has fired and translate any resulting exception into an OperationCanceledException before ShouldRetryLiveSession is consulted, so an internal timeout always propagates immediately while genuine transport resets observed before the timeout still retry as before. Added unit tests (TpapTransportRetryPolicyTests) covering ShouldRetryLiveSession's retry classification and the timeout-to-cancellation translation.
Full Changelog: v1.1.8...v1.1.9
v1.1.8
Fix TpapTransport still absorbing a caller's cancellation/timeout: ShouldRetryLiveSession unconditionally treated TaskCanceledException (and an HttpRequestException message containing "A task was canceled") as retryable, and the previous 1.1.5 fix only guarded against the caller's outer CancellationToken already being canceled. SendOnceAsync races each request against its own internal per-request timeout, separate from the caller's outer token; when that internal timeout fired first, the outer token was not yet canceled, so the retry path still triggered a full session Reset() and a brand-new PAKE handshake (whose PBKDF2/EC math performs no cancellation checks at all), potentially running far longer than the caller's configured deadline before an OperationCanceledException finally surfaced. TaskCanceledException/OperationCanceledException (and the related HttpRequestException message) are no longer treated as retryable by ShouldRetryLiveSession; only genuine connection-reset/abort signals and explicit TPAP protocol session-error codes trigger a session reset and retry, so a timeout or cancellation now propagates immediately.
Full Changelog: v1.1.7...v1.1.8
v1.1.7
Fixes KlapTransport not honoring the configured DeviceConfiguration.Timeout on .NET 10.
The non-net472 request path in KlapTransport (used for KLAP handshake and encrypted requests) called the shared static HttpClient.SendAsync using only whatever CancellationToken the caller supplied, with no per-call deadline derived from DeviceConfiguration.Timeout -- unlike the net472 HttpWebRequest fallback (which sets request.Timeout/ReadWriteTimeout) and TpapTransport (which wraps every request in a per-call timeout via CreateOperationTimeoutSource). Because the shared HttpClient itself has no Timeout configured, a caller that did not supply its own deadline (e.g. CancellationToken.None) would silently fall back to the .NET runtime's default HttpClient timeout of about 100 seconds instead of the configured value.
KlapTransport now wraps every non-net472 request with a per-call timeout derived from DeviceConfiguration.Timeout, matching the behavior already present in TpapTransport and the net472 KLAP fallback. This completes coverage of the originally reported issue: none of the three transports (KLAP, TPAP, Legacy) now silently ignore the caller-configured Timeout on any supported target framework.
Full Changelog: v1.1.6...v1.1.7
v1.1.6
Fixes LegacyTransport (used by KL130 and other legacy XOR/TCP-protocol devices) not honoring a connect timeout/cancellation promptly.
LegacyTransport.ConnectAsync raced TcpClient.ConnectAsync(host, port) against Task.Delay(timeout, cancellationToken) via Task.WhenAny. On .NET Framework, TcpClient.ConnectAsync(string, int) performs synchronous DNS resolution (Dns.GetHostAddresses) on the calling thread before the returned Task even exists and before the WhenAny race is set up. If DNS resolution was slow or hanging, the connect attempt would block the calling method directly, bypassing the timeout/cancellation race entirely -- so a configured StartupConnectTimeout or an external CancellationToken would not be honored until the underlying synchronous DNS call itself eventually returned.
The connect call is now dispatched to a background thread on .NET Framework 4.7.2 so the synchronous DNS phase cannot delay the timeout race, and .NET 10+ uses the cancellation-aware TcpClient.ConnectAsync(host, port, cancellationToken) overload directly. This complements the v1.1.4 (KlapTransport) and v1.1.5 (TpapTransport) fixes, completing cancellation/timeout coverage across all three transports (KLAP, TPAP, and Legacy).
Full Changelog: v1.1.5...v1.1.6
v1.1.5
Fixes TpapTransport swallowing the caller's own cancellation request.
TpapTransport.SendAsync (and the periodic keepalive path) caught TaskCanceledException as a generically 'retryable' live-session condition via ShouldRetryLiveSession, and unconditionally reset the session and retried the request -- including a full handshake -- whenever that exception type was seen. This happened even when the TaskCanceledException was actually caused by the caller's own external CancellationToken being canceled, rather than the transport's internal per-request timeout. As a result, an external cancellation request (e.g. a driver requesting a 20 second cancellation) could be silently absorbed by a retry attempt instead of propagating immediately, so the caller would still observe the full duration of a brand-new handshake+request cycle rather than a prompt cancellation.
Both SendAsync and SendKeepAliveCoreAsync now check cancellationToken.IsCancellationRequested before retrying: a genuine external cancellation now propagates immediately as OperationCanceledException instead of triggering a session reset and retry. This is a more impactful fix than the v1.1.4 KlapTransport change for scenarios that use the TPAP transport (Tapo devices).
Full Changelog: v1.1.4...v1.1.5
v1.1.4
Fixes a cancellation gap introduced in v1.1.3.
On .NET Framework 4.7.2, KlapTransport's HttpWebRequest-based fallback registered HttpWebRequest.Abort() against the caller's CancellationToken, but Abort() does not always promptly unblock an in-flight GetRequestStreamAsync/GetResponseAsync call. As a result, a canceled KLAP request could still block until the request's own configured Timeout elapsed, instead of returning as soon as the token was canceled -- for example, a caller requesting a 20 second cancellation could still see the call block for a much longer runtime-default duration if the request was already in-flight (rather than failing fast on connect).
The net472 fallback now races the underlying GetRequestStreamAsync/GetResponseAsync calls directly against the CancellationToken (via Task.WhenAny) and throws OperationCanceledException immediately once the token fires, independent of whether/when Abort() actually unblocks the original call. The non-net472 HttpClient-based path and TpapTransport were not affected by this specific gap.
Full Changelog: v1.1.3...v1.1.4
v1.1.3
Fixes missing request timeouts and cancellation support in two HTTP code paths:
- KlapTransport (.NET Framework 4.7.2 fallback): The HttpWebRequest-based fallback used only on net472 previously relied on the runtime default timeout (~100 seconds) instead of the configured DeviceConfiguration.Timeout, and could not be reliably cancelled once a request was in flight. It now sets Timeout/ReadWriteTimeout from the configured value, aborts the request when the CancellationToken fires, and translates the resulting WebException into an OperationCanceledException only when cancellation was actually requested (avoiding misclassifying genuine network failures as cancellation).
- TpapTransport keepalive: Periodic keepalive requests sent to maintain a warm TPAP session previously used the shared HttpClient's infinite timeout directly, bypassing the configured timeout and cancellation. Keepalive requests now use the same per-call timeout/cancellation wrapper used by other TPAP requests.
See README.md for details (new 'Request Timeouts and Cancellation' section).
Full Changelog: v1.1.2...v1.1.3