1
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user