2026-08-06 12:58:54 -05:00
|
|
|
using System.Collections.Concurrent;
|
|
|
|
|
|
|
|
|
|
namespace SafelyYou;
|
|
|
|
|
|
|
|
|
|
public readonly record struct DeviceId(string Value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public sealed class KnownDevices
|
|
|
|
|
{
|
|
|
|
|
private readonly HashSet<DeviceId> _knownDevices;
|
|
|
|
|
|
|
|
|
|
public KnownDevices(HashSet<DeviceId> knownDevices) => _knownDevices = knownDevices;
|
|
|
|
|
|
|
|
|
|
public bool Contains(DeviceId deviceId) => _knownDevices.Contains(deviceId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed class DeviceHeartBeatHistory
|
|
|
|
|
{
|
2026-08-06 13:44:58 -05:00
|
|
|
|
2026-08-06 12:58:54 -05:00
|
|
|
public enum AddResult { Added, OutOfSequence }
|
|
|
|
|
|
2026-08-06 13:40:00 -05:00
|
|
|
public long NumberOfHeartBeats { get; private set; }
|
|
|
|
|
public DateTimeOffset? FirstHeartBeat { get; private set; }
|
|
|
|
|
public DateTimeOffset? LastHeartBeat { get; private set; }
|
2026-08-06 12:58:54 -05:00
|
|
|
|
2026-08-06 13:44:58 -05:00
|
|
|
|
2026-08-06 13:40:00 -05:00
|
|
|
public AddResult Add(DateTimeOffset sentAt)
|
2026-08-06 12:58:54 -05:00
|
|
|
{
|
2026-08-06 13:40:00 -05:00
|
|
|
// Reject heartbeats that arrive before the latest registered time.
|
|
|
|
|
// Gaps are fine: uptime can be derived from first, last, and count.
|
|
|
|
|
if (LastHeartBeat is null)
|
|
|
|
|
{
|
|
|
|
|
FirstHeartBeat = sentAt;
|
|
|
|
|
LastHeartBeat = sentAt;
|
|
|
|
|
NumberOfHeartBeats = 1;
|
|
|
|
|
return AddResult.Added;
|
|
|
|
|
}
|
2026-08-06 12:58:54 -05:00
|
|
|
|
2026-08-06 13:40:00 -05:00
|
|
|
if (sentAt < LastHeartBeat)
|
|
|
|
|
return AddResult.OutOfSequence;
|
2026-08-06 12:58:54 -05:00
|
|
|
|
2026-08-06 13:40:00 -05:00
|
|
|
// note: does not account for rejecting heartbeats that come between the one minute window.
|
|
|
|
|
LastHeartBeat = sentAt;
|
|
|
|
|
NumberOfHeartBeats++;
|
2026-08-06 12:58:54 -05:00
|
|
|
return AddResult.Added;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public sealed class HeartBeatHistories
|
|
|
|
|
{
|
|
|
|
|
private readonly ConcurrentDictionary<DeviceId, DeviceHeartBeatHistory> _histories = new();
|
|
|
|
|
|
|
|
|
|
public DeviceHeartBeatHistory.AddResult Add(DeviceId deviceId, DateTimeOffset sentAt)
|
|
|
|
|
{
|
|
|
|
|
if (!_histories.TryGetValue(deviceId, out var history))
|
|
|
|
|
{
|
2026-08-06 13:40:00 -05:00
|
|
|
history = new();
|
2026-08-06 12:58:54 -05:00
|
|
|
_histories[deviceId] = history;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return history.Add(sentAt);
|
|
|
|
|
}
|
2026-08-06 13:40:00 -05:00
|
|
|
|
|
|
|
|
public double Uptime(DeviceId deviceId)
|
|
|
|
|
{
|
|
|
|
|
if (!_histories.TryGetValue(deviceId, out var history)) return 0;
|
|
|
|
|
|
|
|
|
|
var heartBeatMinutes =
|
|
|
|
|
(history.FirstHeartBeat, history.LastHeartBeat) switch
|
|
|
|
|
{
|
|
|
|
|
({} first, {} second ) => (second - first).TotalMinutes,
|
|
|
|
|
(not null, null) => 1,
|
|
|
|
|
(null, _) => 0
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (heartBeatMinutes == 0) return 0L;
|
|
|
|
|
|
|
|
|
|
var uptime = (history.NumberOfHeartBeats / heartBeatMinutes) * 100L;
|
|
|
|
|
return uptime;
|
|
|
|
|
}
|
2026-08-06 12:58:54 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|