Files
Evergreen-Health/Evergreen.Health.Application/Health/Sleep/ReservedSleepWorkflow.cs
T
2026-11-09 16:10:05 +08:00

76 lines
3.7 KiB
C#

namespace Evergreen.Health.Application.Health.Sleep;
public enum ReservedSleepPhase { Disabled, WaitingForBed, Settling, Guiding, Music, Stopped }
public enum SleepWorkflowActionKind { PrepareEnvironment = 0, RequestGuidance = 2, RestartMusicAtFifty = 3, StartLightFade = 4 }
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.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;
}
}