Files
Evergreen-Health/Evergreen.Health.Application/Health/Integrations/Ib43/Ib43MqttService.cs
T
2026-11-11 16:11:41 +08:00

107 lines
6.1 KiB
C#

using Evergreen.Health.Application.Health.Sleep;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Evergreen.Health.Application.Health.Integrations.Ib43;
public sealed class Ib43MqttService(IOptions<Ib43Options> options, IIb43MqttTransportFactory factory,
Ib43StateStore store, TimeProvider clock, ILogger<Ib43MqttService> logger, IIb43ReportSink sink = null,
IBedPresenceSink presenceSink = null) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var configuration = options.Value;
logger.LogInformation("IB43 MQTT startup: Enabled={Enabled}, Host={Host}, Port={Port}, PasswordConfigured={PasswordConfigured}, Devices={Count}",
configuration.Enabled, configuration.Host, configuration.Port,
!string.IsNullOrWhiteSpace(configuration.Password), configuration.Devices?.Count ?? 0);
if (!configuration.Enabled)
{
logger.LogInformation("IB43 MQTT disabled; set Ib43__Enabled=true to enable");
return;
}
// 即使直接构造(测试/其他宿主)也不带错误配置进入重试。
if (!configuration.IsValid()) throw new InvalidOperationException("Invalid Ib43 configuration");
var failures = 0;
while (!stoppingToken.IsCancellationRequested)
{
var connectedAt = clock.GetUtcNow();
var connectionId = Guid.NewGuid();
using var connection = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
var connectionToken = connection.Token;
try
{
using var transport = factory.Create();
var seenKinds = new HashSet<string>(StringComparer.Ordinal);
int? lastMatchedBeds = null;
transport.Message += async (topic, payload, retained) =>
{
var report = store.Receive(topic, payload, retained);
if (sink != null && report.Accepted && report.Readings.Count > 0)
{
var binding = configuration.Devices.Single(d => d.PostTopic == topic);
var reportId = Guid.NewGuid().ToString("N");
Ib43SavedReport saved;
// 等待持久化再结束回调(MQTT 自动 ACK 在此之后)。失败时阻塞重试而非静默丢弃。
while (true)
{
stoppingToken.ThrowIfCancellationRequested();
try
{
saved = await sink.SaveAsync(reportId, binding, report, stoppingToken);
break;
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { throw; }
catch (Exception error)
{
logger.LogWarning("IB43 persistence failed ({Type}); retrying in 5 seconds", error.GetType().Name);
await Task.Delay(TimeSpan.FromSeconds(5), clock, stoppingToken);
}
}
if (saved.MatchedCount != lastMatchedBeds)
logger.LogInformation("IB43 persistence: matched beds={Count}; zero means no user observation saved", saved.MatchedCount);
lastMatchedBeds = saved.MatchedCount;
// 发布失败与写库重试分开,联动问题不能导致重写已保存的报文。
try { Ib43PresenceAdapter.Publish(presenceSink, binding, report, saved.ActiveBeds, connectionId, connectionToken); }
catch (Exception error)
{
logger.LogWarning("IB43 presence publication failed ({Type})", error.GetType().Name);
}
}
if (!report.Accepted || report.Issues.Count > 0)
logger.LogDebug("IB43 report has {Count} parsing issues", report.Issues.Count);
if (report.Accepted && (report.Readings.Count > 0 || report.Fields.Count > 0))
foreach (var kind in report.Kinds)
if (seenKinds.Add(kind))
logger.LogInformation("IB43 first {Kind} report received in this connection", kind);
};
transport.OversizedMessage += store.RejectOversized;
store.SetTransport("connecting");
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(stoppingToken);
timeout.CancelAfter(TimeSpan.FromSeconds(15));
await transport.ConnectAndSubscribeAsync(configuration, timeout.Token);
store.SetTransport("connected");
connectedAt = clock.GetUtcNow();
logger.LogInformation("IB43 MQTT connected and subscribed to {Count} configured devices", configuration.Devices.Count);
while (transport.IsConnected)
await Task.Delay(TimeSpan.FromSeconds(1), clock, stoppingToken);
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; }
catch (Exception exception)
{
// 不写密码、连接选项、完整异常文本或原始健康报文。
logger.LogWarning("IB43 MQTT connection/subscription failed ({Type}); retrying", exception.GetType().Name);
}
finally
{
connection.Cancel();
store.SetTransport("disconnected");
}
if (clock.GetUtcNow() - connectedAt > TimeSpan.FromMinutes(1)) failures = 0;
var seconds = Math.Min(30, Math.Pow(2, Math.Min(failures++, 5)));
try { await Task.Delay(TimeSpan.FromSeconds(seconds), clock, stoppingToken); }
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested) { break; }
}
}
}