77 lines
3.8 KiB
C#
77 lines
3.8 KiB
C#
namespace Evergreen.Health.Application.Health.Sleep;
|
|
|
|
public enum ReservedSleepPhase { Disabled, WaitingForBed, Settling, Guiding, Music, Stopped }
|
|
public enum SleepWorkflowActionKind { PrepareEnvironment, StartGentleRightMassage, RequestGuidance, RestartMusicAtFifty, StartLightFade }
|
|
public sealed record SleepWorkflowAction(SleepWorkflowActionKind Kind, GuidanceClip? Clip = null, int? VolumePercent = null);
|
|
|
|
/// <summary>
|
|
/// 只规划未来动作:没有 HostedService、设备命令或 API 路由,本版默认 Disabled。
|
|
/// 实际接入时由场景协调层消费动作,不能从传感器适配器直接控制家具。
|
|
/// </summary>
|
|
public sealed class ReservedSleepWorkflow
|
|
{
|
|
private readonly SleepWorkflowOptions _options;
|
|
private readonly GuidanceCatalog _guidance;
|
|
private DateTimeOffset? _occupiedSince;
|
|
private DateTimeOffset? _fadeAt;
|
|
private GuidanceClip? _pendingClip;
|
|
private bool _fadeIssued;
|
|
private bool _initialGuide;
|
|
public ReservedSleepPhase Phase { get; private set; } = ReservedSleepPhase.Disabled;
|
|
|
|
public ReservedSleepWorkflow(SleepWorkflowOptions options, GuidanceCatalog guidance)
|
|
=> (_options, _guidance) = (options, guidance);
|
|
|
|
public IReadOnlyList<SleepWorkflowAction> Start()
|
|
{
|
|
_occupiedSince = null; _fadeAt = null; _pendingClip = null; _fadeIssued = false; _initialGuide = false;
|
|
Phase = _options.Enabled ? ReservedSleepPhase.WaitingForBed : ReservedSleepPhase.Disabled;
|
|
return _options.Enabled ? [new(SleepWorkflowActionKind.PrepareEnvironment)] : [];
|
|
}
|
|
|
|
public IReadOnlyList<SleepWorkflowAction> Observe(SleepObservation observation, DateTimeOffset now)
|
|
{
|
|
if (Phase is not (ReservedSleepPhase.WaitingForBed or ReservedSleepPhase.Settling)) return [];
|
|
var fresh = observation.Availability == SleepDataAvailability.Available && observation.ObservedAt is not null &&
|
|
now - observation.ObservedAt <= _options.MaximumObservationAge && observation.ObservedAt <= now.AddMinutes(1);
|
|
if (!fresh || observation.Presence != BedPresence.Occupied)
|
|
{
|
|
_occupiedSince = null; Phase = ReservedSleepPhase.WaitingForBed; return [];
|
|
}
|
|
_occupiedSince ??= now;
|
|
Phase = ReservedSleepPhase.Settling;
|
|
if (now - _occupiedSince < _options.StableBedDuration) return [];
|
|
_pendingClip = _guidance.InitialRelaxation;
|
|
_initialGuide = true;
|
|
Phase = ReservedSleepPhase.Guiding;
|
|
return [new(SleepWorkflowActionKind.StartGentleRightMassage),
|
|
new(SleepWorkflowActionKind.RequestGuidance, _pendingClip, _options.GuidanceVolumePercent)];
|
|
}
|
|
|
|
public IReadOnlyList<SleepWorkflowAction> GuidanceCompleted(string clipKey, bool completed, DateTimeOffset now)
|
|
{
|
|
if (Phase != ReservedSleepPhase.Guiding || !completed || _pendingClip?.Key != clipKey || !_pendingClip.Playable) return [];
|
|
Phase = ReservedSleepPhase.Music;
|
|
if (_initialGuide) _fadeAt = now + _options.PostGuidanceFadeDelay;
|
|
return [new(SleepWorkflowActionKind.RestartMusicAtFifty, VolumePercent: 50)];
|
|
}
|
|
|
|
public IReadOnlyList<SleepWorkflowAction> RequestReminder(GuidanceClip clip)
|
|
{
|
|
if (!_options.Enabled || Phase != ReservedSleepPhase.Music) return [];
|
|
_initialGuide = false; _pendingClip = clip; Phase = ReservedSleepPhase.Guiding;
|
|
return [new(SleepWorkflowActionKind.RequestGuidance, clip, _options.GuidanceVolumePercent)];
|
|
}
|
|
|
|
public IReadOnlyList<SleepWorkflowAction> Tick(DateTimeOffset now)
|
|
{
|
|
if (Phase != ReservedSleepPhase.Music || _fadeIssued || _fadeAt is null || now < _fadeAt) return [];
|
|
_fadeIssued = true; return [new(SleepWorkflowActionKind.StartLightFade)];
|
|
}
|
|
|
|
public void Stop()
|
|
{
|
|
Phase = ReservedSleepPhase.Stopped; _occupiedSince = null; _fadeAt = null; _pendingClip = null;
|
|
}
|
|
}
|