Skip to content

Commit ee26646

Browse files
HandyS11claude
andcommitted
refactor(devices): widen connection reads/actuation to carry DeviceReachability
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 91fe1e6 commit ee26646

4 files changed

Lines changed: 92 additions & 63 deletions

File tree

src/RustPlusBot.Features.Connections/Listening/IRustServerConnection.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,41 +49,41 @@ internal interface IRustServerConnection : IAsyncDisposable
4949
/// <returns>True if the promotion succeeded; false on failure/timeout.</returns>
5050
Task<bool> PromoteToLeaderAsync(ulong steamId, TimeSpan timeout, CancellationToken cancellationToken);
5151

52-
/// <summary>Reads a smart device's on/off state, or null on failure/timeout. Also primes the socket's interest in the entity (so triggers fire for it thereafter).</summary>
52+
/// <summary>Reads a smart device's on/off state and reachability. Also primes the socket's interest in the entity (so triggers fire for it thereafter).</summary>
5353
/// <param name="entityId">The in-game entity id (switch or alarm).</param>
5454
/// <param name="timeout">How long to wait for the response.</param>
5555
/// <param name="cancellationToken">A cancellation token.</param>
56-
/// <returns>True/false for on/off, or null on failure/timeout.</returns>
57-
Task<bool?> GetSmartDeviceInfoAsync(ulong entityId, TimeSpan timeout, CancellationToken cancellationToken);
56+
/// <returns>A <see cref="DeviceReading"/> with the active state (non-null only when <see cref="DeviceReachability.Reachable"/>) and the reachability outcome.</returns>
57+
Task<DeviceReading> GetSmartDeviceInfoAsync(ulong entityId, TimeSpan timeout, CancellationToken cancellationToken);
5858

59-
/// <summary>Reads a storage monitor's contents, or null on failure/timeout. Also primes the socket's interest so triggers fire for it thereafter.</summary>
59+
/// <summary>Reads a storage monitor's contents and reachability. Also primes the socket's interest so triggers fire for it thereafter.</summary>
6060
/// <param name="entityId">The in-game storage-monitor entity id.</param>
6161
/// <param name="timeout">How long to wait for the response.</param>
6262
/// <param name="cancellationToken">A cancellation token.</param>
63-
/// <returns>The contents snapshot, or null on failure/timeout.</returns>
64-
Task<StorageContentsSnapshot?> GetStorageMonitorInfoAsync(ulong entityId,
63+
/// <returns>A <see cref="StorageReading"/> with the contents snapshot (non-null only when <see cref="DeviceReachability.Reachable"/>) and the reachability outcome.</returns>
64+
Task<StorageReading> GetStorageMonitorInfoAsync(ulong entityId,
6565
TimeSpan timeout,
6666
CancellationToken cancellationToken);
6767

68-
/// <summary>Sets a smart switch on/off; returns true on success, false on failure/timeout.</summary>
68+
/// <summary>Sets a smart switch on/off; returns the reachability outcome.</summary>
6969
/// <param name="entityId">The in-game smart-switch entity id.</param>
7070
/// <param name="value">True to turn on, false to turn off.</param>
7171
/// <param name="timeout">How long to wait for the response.</param>
7272
/// <param name="cancellationToken">A cancellation token.</param>
73-
/// <returns>True on success; false on failure/timeout.</returns>
74-
Task<bool> SetSmartSwitchValueAsync(ulong entityId,
73+
/// <returns>The <see cref="DeviceReachability"/> outcome of the operation.</returns>
74+
Task<DeviceReachability> SetSmartSwitchValueAsync(ulong entityId,
7575
bool value,
7676
TimeSpan timeout,
7777
CancellationToken cancellationToken);
7878

79-
/// <summary>Strobes a smart switch; returns true on success, false on failure/timeout.</summary>
79+
/// <summary>Strobes a smart switch; returns the reachability outcome.</summary>
8080
/// <param name="entityId">The in-game smart-switch entity id.</param>
8181
/// <param name="timeoutMs">The in-game strobe duration in milliseconds.</param>
8282
/// <param name="value">The terminal value after strobing.</param>
8383
/// <param name="timeout">How long to wait for the response.</param>
8484
/// <param name="cancellationToken">A cancellation token.</param>
85-
/// <returns>True on success; false on failure/timeout.</returns>
86-
Task<bool> StrobeSmartSwitchAsync(ulong entityId,
85+
/// <returns>The <see cref="DeviceReachability"/> outcome of the operation.</returns>
86+
Task<DeviceReachability> StrobeSmartSwitchAsync(ulong entityId,
8787
int timeoutMs,
8888
bool value,
8989
TimeSpan timeout,

src/RustPlusBot.Features.Connections/Listening/RustPlusSocketSource.cs

Lines changed: 38 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -50,28 +50,28 @@ public Task SendTeamMessageAsync(string message, CancellationToken cancellationT
5050
public Task<bool> PromoteToLeaderAsync(ulong steamId, TimeSpan timeout, CancellationToken cancellationToken) =>
5151
Task.FromResult(false);
5252

53-
public Task<bool?> GetSmartDeviceInfoAsync(ulong entityId,
53+
public Task<DeviceReading> GetSmartDeviceInfoAsync(ulong entityId,
5454
TimeSpan timeout,
5555
CancellationToken cancellationToken) =>
56-
Task.FromResult<bool?>(null);
56+
Task.FromResult(new DeviceReading(null, DeviceReachability.NoResponse));
5757

58-
public Task<StorageContentsSnapshot?> GetStorageMonitorInfoAsync(ulong entityId,
58+
public Task<StorageReading> GetStorageMonitorInfoAsync(ulong entityId,
5959
TimeSpan timeout,
6060
CancellationToken cancellationToken) =>
61-
Task.FromResult<StorageContentsSnapshot?>(null);
61+
Task.FromResult(new StorageReading(null, DeviceReachability.NoResponse));
6262

63-
public Task<bool> SetSmartSwitchValueAsync(ulong entityId,
63+
public Task<DeviceReachability> SetSmartSwitchValueAsync(ulong entityId,
6464
bool value,
6565
TimeSpan timeout,
6666
CancellationToken cancellationToken) =>
67-
Task.FromResult(false);
67+
Task.FromResult(DeviceReachability.NoResponse);
6868

69-
public Task<bool> StrobeSmartSwitchAsync(ulong entityId,
69+
public Task<DeviceReachability> StrobeSmartSwitchAsync(ulong entityId,
7070
int timeoutMs,
7171
bool value,
7272
TimeSpan timeout,
7373
CancellationToken cancellationToken) =>
74-
Task.FromResult(false);
74+
Task.FromResult(DeviceReachability.NoResponse);
7575

7676
public Task<IReadOnlyList<MapMarkerSnapshot>> GetMapMarkersAsync(TimeSpan timeout,
7777
CancellationToken cancellationToken = default) =>
@@ -363,7 +363,7 @@ public async Task<bool> PromoteToLeaderAsync(ulong steamId,
363363

364364
public event EventHandler<StorageMonitorTrigger>? StorageMonitorTriggered;
365365

366-
public async Task<bool?> GetSmartDeviceInfoAsync(ulong entityId,
366+
public async Task<DeviceReading> GetSmartDeviceInfoAsync(ulong entityId,
367367
TimeSpan timeout,
368368
CancellationToken cancellationToken)
369369
{
@@ -375,25 +375,28 @@ public async Task<bool> PromoteToLeaderAsync(ulong steamId,
375375
// Task of Response of SmartDeviceInfo; Response.IsSuccess and Response.Data are the accessors and
376376
// SmartDeviceInfo.IsActive is a bool. The call also primes the socket's interest in this
377377
// entity, so OnSmartDeviceTriggered fires for it thereafter.
378+
// CONFIRMED: Response.Error is ErrorMessage? (null on success), so response.Error?.Code is correct.
378379
var response = await _rustPlus.GetSmartSwitchInfoAsync(entityId, timeoutCts.Token)
379380
.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
380-
return response.IsSuccess && response.Data is { } info ? info.IsActive : null;
381+
var reachability = ReachabilityMapping.FromResponse(response.IsSuccess, response.Error?.Code);
382+
var isActive = response is { IsSuccess: true, Data: { } info } ? info.IsActive : (bool?)null;
383+
return new DeviceReading(isActive, reachability);
381384
}
382385
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
383386
{
384-
return null;
387+
return new DeviceReading(null, DeviceReachability.NoResponse);
385388
}
386-
#pragma warning disable CA1031 // Broad catch: any switch-info failure maps to null; never surface a token/secret.
389+
#pragma warning disable CA1031 // Broad catch: a failed read maps to NoResponse; never surface a token/secret.
387390
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
388391
#pragma warning restore CA1031
389392
{
390393
LogQueryFailed(_logger, ex);
391-
return null;
394+
return new DeviceReading(null, DeviceReachability.NoResponse);
392395
}
393396
}
394397

395398
/// <inheritdoc />
396-
public async Task<StorageContentsSnapshot?> GetStorageMonitorInfoAsync(
399+
public async Task<StorageReading> GetStorageMonitorInfoAsync(
397400
ulong entityId,
398401
TimeSpan timeout,
399402
CancellationToken cancellationToken)
@@ -405,23 +408,27 @@ public async Task<bool> PromoteToLeaderAsync(ulong steamId,
405408
// CONFIRMED (2.0.0-beta.3): GetStorageMonitorInfoAsync(ulong, CancellationToken) returns
406409
// Task<Response<StorageMonitorInfo?>>; the read also primes the entity so OnStorageMonitorTriggered
407410
// fires for it thereafter.
411+
// CONFIRMED: Response.Error is ErrorMessage? (null on success), so response.Error?.Code is correct.
408412
var response = await _rustPlus.GetStorageMonitorInfoAsync(entityId, timeoutCts.Token)
409413
.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
410-
return response is { IsSuccess: true, Data: { } info } ? MapContents(info) : null;
414+
var reachability = ReachabilityMapping.FromResponse(response.IsSuccess, response.Error?.Code);
415+
var contents = response is { IsSuccess: true, Data: { } info } ? MapContents(info) : null;
416+
return new StorageReading(contents, reachability);
411417
}
412418
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
413419
{
414-
return null;
420+
return new StorageReading(null, DeviceReachability.NoResponse);
415421
}
416-
#pragma warning disable CA1031 // Broad catch: a failed/timed-out storage read returns null; the caller treats null as unreachable.
417-
catch (Exception)
422+
#pragma warning disable CA1031 // Broad catch: a failed read maps to NoResponse; never surface a token/secret.
423+
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
418424
#pragma warning restore CA1031
419425
{
420-
return null;
426+
LogQueryFailed(_logger, ex);
427+
return new StorageReading(null, DeviceReachability.NoResponse);
421428
}
422429
}
423430

424-
public async Task<bool> SetSmartSwitchValueAsync(ulong entityId,
431+
public async Task<DeviceReachability> SetSmartSwitchValueAsync(ulong entityId,
425432
bool value,
426433
TimeSpan timeout,
427434
CancellationToken cancellationToken)
@@ -432,24 +439,25 @@ public async Task<bool> SetSmartSwitchValueAsync(ulong entityId,
432439
{
433440
// CONFIRMED (2.0.0-beta.3): SetSmartSwitchValueAsync(ulong, bool, CancellationToken) returns
434441
// Task of Response of SmartDeviceInfo; Response.IsSuccess indicates the outcome.
442+
// CONFIRMED: Response.Error is ErrorMessage? (null on success), so response.Error?.Code is correct.
435443
var response = await _rustPlus.SetSmartSwitchValueAsync(entityId, value, timeoutCts.Token)
436444
.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
437-
return response.IsSuccess;
445+
return ReachabilityMapping.FromResponse(response.IsSuccess, response.Error?.Code);
438446
}
439447
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
440448
{
441-
return false;
449+
return DeviceReachability.NoResponse;
442450
}
443-
#pragma warning disable CA1031 // Broad catch: any set failure maps to false; never surface a token/secret.
451+
#pragma warning disable CA1031 // Broad catch: a failed set maps to NoResponse; never surface a token/secret.
444452
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
445453
#pragma warning restore CA1031
446454
{
447455
LogQueryFailed(_logger, ex);
448-
return false;
456+
return DeviceReachability.NoResponse;
449457
}
450458
}
451459

452-
public async Task<bool> StrobeSmartSwitchAsync(ulong entityId,
460+
public async Task<DeviceReachability> StrobeSmartSwitchAsync(ulong entityId,
453461
int timeoutMs,
454462
bool value,
455463
TimeSpan timeout,
@@ -461,20 +469,21 @@ public async Task<bool> StrobeSmartSwitchAsync(ulong entityId,
461469
{
462470
// CONFIRMED (2.0.0-beta.3): StrobeSmartSwitchAsync(ulong, int timeoutMs, bool value, CancellationToken)
463471
// returns Task of Response of SmartDeviceInfo; Response.IsSuccess indicates the outcome.
472+
// CONFIRMED: Response.Error is ErrorMessage? (null on success), so response.Error?.Code is correct.
464473
var response = await _rustPlus.StrobeSmartSwitchAsync(entityId, timeoutMs, value, timeoutCts.Token)
465474
.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
466-
return response.IsSuccess;
475+
return ReachabilityMapping.FromResponse(response.IsSuccess, response.Error?.Code);
467476
}
468477
catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
469478
{
470-
return false;
479+
return DeviceReachability.NoResponse;
471480
}
472-
#pragma warning disable CA1031 // Broad catch: any strobe failure maps to false; never surface a token/secret.
481+
#pragma warning disable CA1031 // Broad catch: a failed strobe maps to NoResponse; never surface a token/secret.
473482
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
474483
#pragma warning restore CA1031
475484
{
476485
LogQueryFailed(_logger, ex);
477-
return false;
486+
return DeviceReachability.NoResponse;
478487
}
479488
}
480489

src/RustPlusBot.Features.Connections/Supervisor/ConnectionSupervisor.cs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,9 @@ public async Task<IReadOnlyList<MonumentSnapshot>> GetMonumentsAsync(
263263
return null;
264264
}
265265

266-
return await live.Connection.GetSmartDeviceInfoAsync(entityId, _options.HeartbeatTimeout, cancellationToken)
266+
var reading = await live.Connection.GetSmartDeviceInfoAsync(entityId, _options.HeartbeatTimeout, cancellationToken)
267267
.ConfigureAwait(false);
268+
return reading.IsActive;
268269
}
269270

270271
/// <inheritdoc />
@@ -279,9 +280,10 @@ public async Task<IReadOnlyList<MonumentSnapshot>> GetMonumentsAsync(
279280
return null;
280281
}
281282

282-
return await live.Connection
283+
var reading = await live.Connection
283284
.GetStorageMonitorInfoAsync(entityId, _options.HeartbeatTimeout, cancellationToken)
284285
.ConfigureAwait(false);
286+
return reading.Contents;
285287
}
286288

287289
/// <inheritdoc />
@@ -297,9 +299,10 @@ public async Task<bool> SetSmartSwitchAsync(
297299
return false;
298300
}
299301

300-
return await live.Connection
302+
var reachability = await live.Connection
301303
.SetSmartSwitchValueAsync(entityId, value, _options.HeartbeatTimeout, cancellationToken)
302304
.ConfigureAwait(false);
305+
return reachability == DeviceReachability.Reachable;
303306
}
304307

305308
/// <inheritdoc />
@@ -316,9 +319,10 @@ public async Task<bool> StrobeSmartSwitchAsync(
316319
return false;
317320
}
318321

319-
return await live.Connection
322+
var reachability = await live.Connection
320323
.StrobeSmartSwitchAsync(entityId, timeoutMs, value, _options.HeartbeatTimeout, cancellationToken)
321324
.ConfigureAwait(false);
325+
return reachability == DeviceReachability.Reachable;
322326
}
323327

324328
/// <inheritdoc />
@@ -1012,11 +1016,11 @@ private async Task PublishDevicePrimeAsync(
10121016

10131017
try
10141018
{
1015-
var isActive = await connection
1019+
var reading = await connection
10161020
.GetSmartDeviceInfoAsync(entityId, _options.HeartbeatTimeout, _shutdown.Token)
10171021
.ConfigureAwait(false);
10181022
await eventBus.PublishAsync(
1019-
new SmartDeviceTriggeredEvent(key.Guild, key.Server, entityId, isActive ?? false),
1023+
new SmartDeviceTriggeredEvent(key.Guild, key.Server, entityId, reading.IsActive ?? false),
10201024
_shutdown.Token)
10211025
.ConfigureAwait(false);
10221026
}
@@ -1077,10 +1081,10 @@ private async Task PublishStoragePrimeAsync(
10771081

10781082
try
10791083
{
1080-
var contents = await connection
1084+
var reading = await connection
10811085
.GetStorageMonitorInfoAsync(entityId, _options.HeartbeatTimeout, _shutdown.Token)
10821086
.ConfigureAwait(false);
1083-
if (contents is null)
1087+
if (reading.Contents is not { } contents)
10841088
{
10851089
return; // unreachable read; the relay leaves the embed as-is (or unreachable via status events).
10861090
}

0 commit comments

Comments
 (0)