using Evergreen.Health.Application.Health.Models; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace Evergreen.Health.Application.Health.Integrations.HomeAssistant; /// 在服务端驱动卧室灯光渐变,浏览器关闭后任务仍继续。 public sealed class HomeAssistantLightTransitionService : IHostedService { private const int SleepStartPercent = 50; private const int WakeTargetPercent = 70; private static readonly TimeSpan StepInterval = TimeSpan.FromSeconds(5); private readonly HomeAssistantClient _homeAssistant; private readonly HomeAssistantOptions _options; private readonly ILogger _logger; private readonly SemaphoreSlim _operationLock = new(1, 1); private readonly object _statusLock = new(); private CancellationTokenSource? _activeCancellation; private Task? _activeTask; private HomeAssistantLightTransitionOutput _status = new(); public HomeAssistantLightTransitionService( IHttpClientFactory httpClientFactory, IOptions options, ILogger logger) { _options = options.Value; _logger = logger; _homeAssistant = new HomeAssistantClient(httpClientFactory.CreateClient(nameof(HomeAssistantLightTransitionService)), options); } public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; public async Task StopAsync(CancellationToken cancellationToken) { await _operationLock.WaitAsync(cancellationToken); try { await StopCurrentAsync(); } finally { _operationLock.Release(); } } public HomeAssistantLightTransitionOutput GetStatus() { lock (_statusLock) { return new HomeAssistantLightTransitionOutput { Success = _status.Success, Running = _status.Running, Mode = _status.Mode, BrightnessPercent = _status.BrightnessPercent, TargetPercent = _status.TargetPercent, Message = _status.Message }; } } public async Task StartSleepAsync(int colorTemperatureKelvin, CancellationToken cancellationToken) { await _operationLock.WaitAsync(cancellationToken); try { await StopCurrentAsync(); await ReadAvailableLightAsync(cancellationToken); await _homeAssistant.CallServiceAsync("light", "turn_on", new Dictionary { ["entity_id"] = LightEntityId, ["brightness_pct"] = SleepStartPercent, ["color_temp_kelvin"] = Math.Clamp(colorTemperatureKelvin, 2200, 5000) }, cancellationToken); SetStatus(true, true, "sleep", SleepStartPercent, 0, "助眠渐暗已启动,每5秒降低1%亮度"); Launch("sleep", SleepStartPercent); return GetStatus(); } finally { _operationLock.Release(); } } public async Task StartWakeAsync(CancellationToken cancellationToken) { await _operationLock.WaitAsync(cancellationToken); try { await StopCurrentAsync(); var light = await ReadAvailableLightAsync(cancellationToken); var isOff = string.Equals(light.State, "off", StringComparison.OrdinalIgnoreCase); var brightness = light.GetDoubleAttribute("brightness"); if (!isOff && brightness is null) throw new InvalidOperationException("灯具未返回当前亮度,无法从当前亮度启动晨起唤醒"); var current = isOff ? 1 : Math.Clamp((int)Math.Round(brightness!.Value / 255d * 100), 1, 100); if (current >= WakeTargetPercent && !isOff) { SetStatus(true, false, "wake", current, WakeTargetPercent, "当前亮度已达到70%或更高,保持原亮度"); return GetStatus(); } if (isOff) { await _homeAssistant.CallServiceAsync("light", "turn_on", new Dictionary { ["entity_id"] = LightEntityId, ["brightness_pct"] = 1 }, cancellationToken); } SetStatus(true, true, "wake", current, WakeTargetPercent, "晨起唤醒已启动,每5秒增加1%亮度"); Launch("wake", current); return GetStatus(); } finally { _operationLock.Release(); } } public async Task CancelAsync(CancellationToken cancellationToken) { await _operationLock.WaitAsync(cancellationToken); try { await StopCurrentAsync(); var brightness = GetStatus().BrightnessPercent; SetStatus(true, false, "none", brightness, 0, "灯光渐变已停止"); return GetStatus(); } finally { _operationLock.Release(); } } private string LightEntityId => !string.IsNullOrWhiteSpace(_options.Entities.AmbientLight) ? _options.Entities.AmbientLight : throw new InvalidOperationException("卧室灯光实体尚未配置"); private async Task ReadAvailableLightAsync(CancellationToken cancellationToken) { var light = (await _homeAssistant.GetStatesAsync(cancellationToken)) .FirstOrDefault(item => string.Equals(item.EntityId, LightEntityId, StringComparison.OrdinalIgnoreCase)); if (light is null || light.State is "unknown" or "unavailable") throw new InvalidOperationException("卧室灯光当前离线或不可用"); return light; } private void Launch(string mode, int initialPercent) { _activeCancellation = new CancellationTokenSource(); _activeTask = RunAsync(mode, initialPercent, _activeCancellation.Token); } private async Task RunAsync(string mode, int initialPercent, CancellationToken cancellationToken) { var current = initialPercent; try { while (true) { await Task.Delay(StepInterval, cancellationToken); var light = await ReadAvailableLightAsync(cancellationToken); if (!string.Equals(light.State, "on", StringComparison.OrdinalIgnoreCase)) { SetStatus(false, false, mode, 0, mode == "wake" ? WakeTargetPercent : 0, "灯已从其他位置关闭,渐变已停止"); return; } var brightness = light.GetDoubleAttribute("brightness"); if (brightness is null) { SetStatus(false, false, mode, current, mode == "wake" ? WakeTargetPercent : 0, "灯具未返回当前亮度,渐变已停止"); return; } var observed = Math.Clamp((int)Math.Round(brightness.Value / 255d * 100), 1, 100); if (Math.Abs(observed - current) > 2) { SetStatus(false, false, mode, observed, mode == "wake" ? WakeTargetPercent : 0, "灯光亮度已从其他位置修改,渐变已停止"); return; } if (mode == "sleep" && current == 1) { await _homeAssistant.CallServiceAsync("light", "turn_off", new Dictionary { ["entity_id"] = LightEntityId }, cancellationToken); SetStatus(true, false, mode, 0, 0, "助眠渐暗已完成,灯光已关闭"); return; } var next = mode == "sleep" ? current - 1 : current + 1; await _homeAssistant.CallServiceAsync("light", "turn_on", new Dictionary { ["entity_id"] = LightEntityId, ["brightness_pct"] = next }, cancellationToken); current = next; var finished = mode == "wake" && current >= WakeTargetPercent; SetStatus(true, !finished, mode, current, mode == "wake" ? WakeTargetPercent : 0, finished ? "晨起唤醒已完成,灯光保持70%亮度" : mode == "wake" ? "晨起唤醒进行中" : "助眠渐暗进行中"); if (finished) return; } } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { } catch (Exception exception) { _logger.LogWarning(exception, "卧室灯光渐变任务中断"); SetStatus(false, false, mode, current, mode == "wake" ? WakeTargetPercent : 0, $"灯光渐变中断:{exception.Message}"); } } private async Task StopCurrentAsync() { if (_activeCancellation is null) return; _activeCancellation.Cancel(); if (_activeTask is not null) await _activeTask; _activeCancellation.Dispose(); _activeCancellation = null; _activeTask = null; SetStatus(true, false, "none", GetStatus().BrightnessPercent, 0, "灯光渐变已停止"); } private void SetStatus(bool success, bool running, string mode, int brightness, int target, string message) { lock (_statusLock) { _status = new HomeAssistantLightTransitionOutput { Success = success, Running = running, Mode = mode, BrightnessPercent = brightness, TargetPercent = target, Message = message }; } } }