using Evergreen.Health.Application.Health.Devices.Contracts; using Evergreen.Health.Application.Health.Devices; using Evergreen.Health.Application.Health.Integrations.HomeAssistant; using Evergreen.Health.Application.Health.Integrations.MiAirX; using Evergreen.Health.Application.Health.Models; using Evergreen.Health.Application.Health.Scenes; using System.Collections.Concurrent; using System.Text.Json; namespace Admin.NET.Test.Health; internal sealed class FakeHaGateway : IHomeAssistantGateway { public ConcurrentDictionary States { get; } = new(StringComparer.OrdinalIgnoreCase); public ConcurrentQueue Calls { get; } = new(); public string? FailEntity { get; set; } public int ReadsToFail { get; set; } public TimeProvider Time { get; set; } = TimeProvider.System; public Task> GetStatesAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); if (ReadsToFail > 0) { ReadsToFail--; throw new IOException("模拟状态读取失败"); } return Task.FromResult>(States.Values.ToArray()); } public Task CallServiceAsync(string domain, string service, IReadOnlyDictionary data, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var entity = (string)data["entity_id"]!; Calls.Enqueue(new(service, entity, domain, service, new Dictionary(data))); if (entity == FailEntity) throw new IOException("模拟设备命令失败"); var state = service switch { "turn_on" => "on", "turn_off" => "off", "select_option" => data["option"]!.ToString()!, "set_value" => data["value"]!.ToString()!, _ => States.GetValueOrDefault(entity)?.State ?? "unknown" }; if (service == "set_hvac_mode") state = (string)data["hvac_mode"]!; var attributes = new Dictionary(); if (States.TryGetValue(entity, out var previous) && previous.Attributes.ValueKind == JsonValueKind.Object) foreach (var property in previous.Attributes.EnumerateObject()) attributes[property.Name] = property.Value.Clone(); if (data.TryGetValue("brightness_pct", out var brightness)) attributes["brightness"] = Convert.ToDouble(brightness) / 100 * 255; foreach (var key in new[] { "effect", "color_temp_kelvin", "temperature", "fan_mode" }) if (data.TryGetValue(key, out var value)) attributes[key] = value; States[entity] = new() { EntityId = entity, State = state, LastReported = Time.GetUtcNow(), LastUpdated = Time.GetUtcNow(), Attributes = JsonSerializer.SerializeToElement(attributes) }; return Task.CompletedTask; } } internal sealed class FakeMusic : ITcmMusicRuntime { public int StopCount { get; private set; } public TcmMusicTransitionOutput GetStatus() => new(); public bool HasPlayableTracks(string code) => true; public Task StartForConstitutionAsync(string code, CancellationToken ct) => Task.CompletedTask; public Task PlayTestMusicAsync(CancellationToken ct) => Task.CompletedTask; public Task SetManualVolumeAsync(int volume, CancellationToken ct) => Task.CompletedTask; public Task StopMusicAsync(CancellationToken ct) { StopCount++; return Task.CompletedTask; } } internal sealed class FakeLight : ILightTransitionRuntime { public HomeAssistantLightTransitionOutput GetStatus() => new(); public Task StartSleepAsync(int kelvin, CancellationToken ct) => Task.FromResult(new HomeAssistantLightTransitionOutput()); public Task StartWakeAsync(CancellationToken ct) => Task.FromResult(new HomeAssistantLightTransitionOutput()); public Task CancelAsync(CancellationToken ct) => Task.FromResult(new HomeAssistantLightTransitionOutput()); } internal sealed class SyntheticDeviceModule : HealthDeviceModule { private readonly bool _block; private readonly bool _twoOff; private readonly string _key; public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously); public bool ContinuedAfterWait { get; private set; } public SyntheticDeviceModule(DeviceActionRunner runner, bool block = false, bool twoOff = false, string key = "synthetic") : base(runner) => (_block, _twoOff, _key) = (block, twoOff, key); public override string Key => _key; public override string Category => nameof(HealthDeviceCategory.Other); public override string DisplayName => "测试新增家具"; public override string PrimaryEntityId => "switch.synthetic"; public override IReadOnlyList Parameters => [DeviceValues.Number("synthetic_value", "number.synthetic", 0, 5, "测试参数")]; public override async Task ExecuteSleepAsync(DeviceSceneContext context, PersonalizedSceneConfig config, string constitutionCode, CancellationToken ct) { Started.TrySetResult(); if (_block) await Task.Delay(Timeout.InfiniteTimeSpan, ct); ContinuedAfterWait = true; await context.RunAsync(BuildParameterCommand("synthetic_value", 3), ct); } public override IEnumerable GetShutdownCommands() { if (_twoOff) yield return new("失败设备关闭", "switch.bad", "switch", "turn_off", ExpectedState: "off"); yield return new("测试家具关闭", PrimaryEntityId, "switch", "turn_off", ExpectedState: "off"); } } internal sealed class FakeAudioGateway : IAudioPlaybackGateway { public bool IsConfigured => true; public ConcurrentQueue Volumes { get; } = new(); public ConcurrentQueue Tracks { get; } = new(); public int Stops; public Task PlayAsync(CancellationToken ct = default) => Task.CompletedTask; public Task PlayUrlAsync(string url, CancellationToken ct = default) { Tracks.Enqueue(url); return Task.CompletedTask; } public Task StopAsync(CancellationToken ct = default) { Interlocked.Increment(ref Stops); return Task.CompletedTask; } public Task SetVolumeAsync(int volume, CancellationToken ct = default) { Volumes.Enqueue(volume); return Task.CompletedTask; } } /// 只推进测试时间,不等待真实 250 秒,也不依赖额外测试包。 internal sealed class ManualTestTimeProvider : TimeProvider { private readonly object _lock = new(); private readonly List _timers = new(); private readonly DateTimeOffset _origin = DateTimeOffset.UtcNow; private long _ticks; public override long TimestampFrequency => TimeSpan.TicksPerSecond; public override long GetTimestamp() { lock (_lock) return _ticks; } public override DateTimeOffset GetUtcNow() { lock (_lock) return _origin.AddTicks(_ticks); } public override ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period) { var timer = new ManualTimer(this, callback, state); lock (_lock) { _timers.Add(timer); timer.Change(dueTime, period); } return timer; } public void Advance(TimeSpan duration) { List due; lock (_lock) { _ticks += duration.Ticks; due = _timers.Where(timer => !timer.Disposed && timer.Due <= _ticks).ToList(); foreach (var timer in due) timer.Due = timer.Period > 0 ? _ticks + timer.Period : long.MaxValue; } foreach (var timer in due) timer.Callback(timer.State); } private sealed class ManualTimer : ITimer { private readonly ManualTestTimeProvider _owner; public TimerCallback Callback { get; } public object? State { get; } public bool Disposed; public long Due; public long Period; public ManualTimer(ManualTestTimeProvider owner, TimerCallback callback, object? state) => (_owner, Callback, State) = (owner, callback, state); public bool Change(TimeSpan dueTime, TimeSpan period) { lock (_owner._lock) { if (Disposed) return false; Due = dueTime == Timeout.InfiniteTimeSpan ? long.MaxValue : _owner._ticks + dueTime.Ticks; Period = period == Timeout.InfiniteTimeSpan ? 0 : period.Ticks; return true; } } public void Dispose() { lock (_owner._lock) { Disposed = true; _owner._timers.Remove(this); } } public ValueTask DisposeAsync() { Dispose(); return ValueTask.CompletedTask; } } }