diff --git a/Admin.NET.Web.Core/Startup.cs b/Admin.NET.Web.Core/Startup.cs index c5b06ea..0085eb9 100644 --- a/Admin.NET.Web.Core/Startup.cs +++ b/Admin.NET.Web.Core/Startup.cs @@ -76,6 +76,8 @@ public class Startup : AppStartup // 远程请求 services.AddHttpRemote(); services.AddHttpClient(); + services.AddSingleton(); + services.AddHostedService(provider => provider.GetRequiredService()); // 任务队列 services.AddTaskQueue(); // 脱敏检测 diff --git a/Evergreen.Health.Application/Configuration/HomeAssistant.json b/Evergreen.Health.Application/Configuration/HomeAssistant.json index c18e7c9..d2bfdc0 100644 --- a/Evergreen.Health.Application/Configuration/HomeAssistant.json +++ b/Evergreen.Health.Application/Configuration/HomeAssistant.json @@ -9,6 +9,12 @@ "DehumidifierUv": "switch.xiaomi_cn_972783525_60l_uv_p_2_10", "FragranceSwitch": "switch.xiaomi_cn_1206553658_xw002_on_p_2_2", "FragranceLight": "light.xiaomi_cn_1206553658_xw002_s_3_ambient_light", + "FragranceDuration": "number.xiaomi_cn_1206553658_xw002_fragrance_out_time_p_2_3", + "FragranceInterval": "number.xiaomi_cn_1206553658_xw002_fragrance_out_time_p_2_4", + "FragranceQuickRelease": "button.xiaomi_cn_1206553658_xw002_fragrance_delivery_a_4_1", + "FragranceAutoRelease": "switch.xiaomi_cn_1206553658_xw002_auto_fragrance_p_4_1", + "FragranceAutoLight": "switch.xiaomi_cn_1206553658_xw002_auto_light_p_4_2", + "FragranceLightAutoOff": "switch.xiaomi_cn_1206553658_xw002_light_auto_off_p_4_3", "AmbientLight": "light.lemesh_cn_2041198316_wy0d02_s_2_light", "Speaker": "media_player.xiaomi_cn_1221084723_l17a", "SpeakerSleepMode": "switch.xiaomi_cn_1221084723_l17a_sleep_mode_p_7_3", diff --git a/Evergreen.Health.Application/Health/Integrations/HomeAssistant/HomeAssistantLightTransitionService.cs b/Evergreen.Health.Application/Health/Integrations/HomeAssistant/HomeAssistantLightTransitionService.cs new file mode 100644 index 0000000..156531b --- /dev/null +++ b/Evergreen.Health.Application/Health/Integrations/HomeAssistant/HomeAssistantLightTransitionService.cs @@ -0,0 +1,233 @@ +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 + }; + } + } +} diff --git a/Evergreen.Health.Application/Health/Integrations/HomeAssistant/HomeAssistantOptions.cs b/Evergreen.Health.Application/Health/Integrations/HomeAssistant/HomeAssistantOptions.cs index 0ea9e0c..b18e2e9 100644 --- a/Evergreen.Health.Application/Health/Integrations/HomeAssistant/HomeAssistantOptions.cs +++ b/Evergreen.Health.Application/Health/Integrations/HomeAssistant/HomeAssistantOptions.cs @@ -19,6 +19,12 @@ public sealed class HomeAssistantEntityOptions public string DehumidifierUv { get; set; } = string.Empty; public string FragranceSwitch { get; set; } = string.Empty; public string FragranceLight { get; set; } = string.Empty; + public string FragranceDuration { get; set; } = string.Empty; + public string FragranceInterval { get; set; } = string.Empty; + public string FragranceQuickRelease { get; set; } = string.Empty; + public string FragranceAutoRelease { get; set; } = string.Empty; + public string FragranceAutoLight { get; set; } = string.Empty; + public string FragranceLightAutoOff { get; set; } = string.Empty; public string AmbientLight { get; set; } = string.Empty; public string Speaker { get; set; } = string.Empty; public string SpeakerSleepMode { get; set; } = string.Empty; diff --git a/Evergreen.Health.Application/Health/Models/HomeAssistantModels.cs b/Evergreen.Health.Application/Health/Models/HomeAssistantModels.cs index d5a3f62..afe1d3c 100644 --- a/Evergreen.Health.Application/Health/Models/HomeAssistantModels.cs +++ b/Evergreen.Health.Application/Health/Models/HomeAssistantModels.cs @@ -113,6 +113,25 @@ public sealed class HomeAssistantDeviceSwitchOutput public string Message { get; set; } = string.Empty; } +public sealed class HomeAssistantLightTransitionOutput +{ + public bool Success { get; set; } = true; + public bool Running { get; set; } + public string Mode { get; set; } = "none"; + public int BrightnessPercent { get; set; } + public int TargetPercent { get; set; } + public string Message { get; set; } = string.Empty; +} + +public sealed class HomeAssistantSpeakerDemoOutput +{ + public string AudioUrl { get; set; } = string.Empty; + public bool SpeakerOnline { get; set; } + public bool DirectPlaybackSupported { get; set; } + public bool CommandAccepted { get; set; } + public string Message { get; set; } = string.Empty; +} + public sealed class HomeAssistantSetParameterInput { public string DeviceId { get; set; } = string.Empty; diff --git a/Evergreen.Health.Application/Health/Services/HealthHomeAssistantService.cs b/Evergreen.Health.Application/Health/Services/HealthHomeAssistantService.cs index 4cea074..75b58af 100644 --- a/Evergreen.Health.Application/Health/Services/HealthHomeAssistantService.cs +++ b/Evergreen.Health.Application/Health/Services/HealthHomeAssistantService.cs @@ -14,7 +14,10 @@ namespace Evergreen.Health.Application.Health.Services; public sealed class HealthHomeAssistantService : IDynamicApiController, ITransient { private const string HealthUserIdClaim = "health_user_id"; + private const int PlayMediaFeature = 512; + private const string DemoAudioUrl = "https://oss.myzxjgt.com/App.Live/uploads/%E9%9F%B3%E9%A2%91/2026-05-29/57a6a64d8dfa4a1bb876a15bb8fdfd24.mp3"; private readonly HomeAssistantClient _homeAssistant; + private readonly HomeAssistantLightTransitionService _lightTransition; private readonly HomeAssistantRegistryService _registryService; private readonly HomeAssistantOptions _options; private readonly SqlSugarRepository _executionRepository; @@ -23,6 +26,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie public HealthHomeAssistantService( HomeAssistantClient homeAssistant, + HomeAssistantLightTransitionService lightTransition, HomeAssistantRegistryService registryService, IOptions options, SqlSugarRepository executionRepository, @@ -30,6 +34,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie IHttpContextAccessor httpContextAccessor) { _homeAssistant = homeAssistant; + _lightTransition = lightTransition; _registryService = registryService; _options = options.Value; _executionRepository = executionRepository; @@ -37,6 +42,97 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie _httpContextAccessor = httpContextAccessor; } + [Authorize] + [HttpGet("/api/health/home-assistant/lights/transition")] + public HomeAssistantLightTransitionOutput GetLightTransitionStatus() => _lightTransition.GetStatus(); + + [Authorize] + [HttpPost("/api/health/home-assistant/lights/sleep")] + public async Task StartSleepLight(CancellationToken cancellationToken) + { + try { return await _lightTransition.StartSleepAsync(_options.SleepScene.LightColorTemperatureKelvin, cancellationToken); } + catch (Exception exception) { return new HomeAssistantLightTransitionOutput { Success = false, Message = exception.Message }; } + } + + [Authorize] + [HttpPost("/api/health/home-assistant/lights/wake")] + public async Task StartWakeLight(CancellationToken cancellationToken) + { + try { return await _lightTransition.StartWakeAsync(cancellationToken); } + catch (Exception exception) { return new HomeAssistantLightTransitionOutput { Success = false, Message = exception.Message }; } + } + + [Authorize] + [HttpPost("/api/health/home-assistant/lights/stop")] + public async Task StopLightTransition(CancellationToken cancellationToken) + { + try { return await _lightTransition.CancelAsync(cancellationToken); } + catch (Exception exception) { return new HomeAssistantLightTransitionOutput { Success = false, Message = exception.Message }; } + } + + [Authorize] + [HttpGet("/api/health/home-assistant/speaker/demo")] + public async Task GetSpeakerDemo(CancellationToken cancellationToken) + { + var result = new HomeAssistantSpeakerDemoOutput { AudioUrl = DemoAudioUrl }; + try + { + var entityId = _options.Entities.Speaker; + if (string.IsNullOrWhiteSpace(entityId)) + { + result.Message = "尚未配置音箱实体"; + return result; + } + + var state = (await _homeAssistant.GetStatesAsync(cancellationToken)) + .FirstOrDefault(item => string.Equals(item.EntityId, entityId, StringComparison.OrdinalIgnoreCase)); + result.SpeakerOnline = state is not null && IsAvailable(state); + if (!result.SpeakerOnline) + { + result.Message = "音箱当前离线或不可用"; + return result; + } + + var features = (long)(state!.GetDoubleAttribute("supported_features") ?? 0); + result.DirectPlaybackSupported = (features & PlayMediaFeature) != 0; + result.Message = result.DirectPlaybackSupported + ? "音箱声明支持指定音频播放,可以尝试 OSS 示例音频" + : "当前 Xiaomi Home 音箱实体不支持播放指定音频;OSS 链接可在浏览器试听,但暂不能投放到音箱"; + } + catch (Exception exception) + { + result.Message = $"音箱能力检测失败:{exception.Message}"; + } + + return result; + } + + [Authorize] + [HttpPost("/api/health/home-assistant/speaker/play-demo")] + public async Task PlaySpeakerDemo(CancellationToken cancellationToken) + { + var result = await GetSpeakerDemo(cancellationToken); + if (!result.SpeakerOnline || !result.DirectPlaybackSupported) return result; + + try + { + await _homeAssistant.CallServiceAsync("media_player", "play_media", new Dictionary + { + ["entity_id"] = _options.Entities.Speaker, + ["media_content_id"] = DemoAudioUrl, + ["media_content_type"] = "music" + }, cancellationToken); + result.CommandAccepted = true; + result.Message = "播放指令已发送,请确认音箱是否实际出声;接口接受不等于播放成功"; + } + catch (Exception exception) + { + result.Message = $"音箱未接受播放指令:{exception.Message}"; + } + + return result; + } + [Authorize] [HttpGet("/api/health/home-assistant/status")] public async Task GetStatus(CancellationToken cancellationToken) @@ -172,6 +268,24 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie }; } + if (domain == "light" && string.Equals(targetEntityId, _options.Entities.AmbientLight, StringComparison.OrdinalIgnoreCase)) + { + if (input.TurnOn) + { + var transition = await _lightTransition.StartSleepAsync(2700, cancellationToken); + return new HomeAssistantDeviceSwitchOutput + { + Success = transition.Success, + DeviceId = input.DeviceId, + EntityId = targetEntityId, + State = "on", + Message = transition.Message + }; + } + + await _lightTransition.CancelAsync(cancellationToken); + } + var targetOn = input.TurnOn; bool? targetMuted = null; var service = targetOn ? "turn_on" : "turn_off"; @@ -354,6 +468,45 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie domain = "switch"; service = ReadBool(input.Value) ? "turn_on" : "turn_off"; break; + case "fragrance_duration": + targetEntityId = _options.Entities.FragranceDuration; + domain = "number"; + service = "set_value"; + serviceData["value"] = ReadInt(input.Value, 2, 6, "出香时长"); + break; + case "fragrance_interval": + targetEntityId = _options.Entities.FragranceInterval; + domain = "number"; + service = "set_value"; + var fragranceInterval = ReadInt(input.Value, 10, 20, "出香间隔"); + if (fragranceInterval % 5 != 0) throw new InvalidOperationException("出香间隔仅支持 10、15 或 20 分钟"); + serviceData["value"] = fragranceInterval; + break; + case "fragrance_light": + targetEntityId = _options.Entities.FragranceLight; + domain = "light"; + service = ReadBool(input.Value) ? "turn_on" : "turn_off"; + break; + case "fragrance_quick_release": + targetEntityId = _options.Entities.FragranceQuickRelease; + domain = "button"; + service = "press"; + break; + case "fragrance_auto_release": + targetEntityId = _options.Entities.FragranceAutoRelease; + domain = "switch"; + service = ReadBool(input.Value) ? "turn_on" : "turn_off"; + break; + case "fragrance_auto_light": + targetEntityId = _options.Entities.FragranceAutoLight; + domain = "switch"; + service = ReadBool(input.Value) ? "turn_on" : "turn_off"; + break; + case "fragrance_light_auto_off": + targetEntityId = _options.Entities.FragranceLightAutoOff; + domain = "switch"; + service = ReadBool(input.Value) ? "turn_on" : "turn_off"; + break; case "mattress_left_heating": targetEntityId = _options.Entities.MattressLeftHeatingLevel; domain = "number"; @@ -403,6 +556,9 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie if (!stateMap.TryGetValue(targetEntityId, out var targetState) || !IsAvailable(targetState)) throw new InvalidOperationException("目标参数实体当前离线或不可用"); + if (parameterType is "brightness" or "color_temp") + await _lightTransition.CancelAsync(cancellationToken); + serviceData["entity_id"] = targetEntityId; await _homeAssistant.CallServiceAsync(domain, service, serviceData, cancellationToken); @@ -442,6 +598,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie var successMessage = parameterType switch { + "fragrance_quick_release" => "快速出香指令已发送", "mattress_left_mode" when !string.Equals(serviceData["option"]?.ToString(), "无", StringComparison.Ordinal) => "左侧承托模式已启动;床垫共用气泵,右侧会自动停止", "mattress_right_mode" when !string.Equals(serviceData["option"]?.ToString(), "无", StringComparison.Ordinal) => @@ -572,6 +729,9 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie var lightBrightness = customConfig?.LightBrightnessPercent ?? scene.LightBrightnessPercent; var lightColorTemp = customConfig?.LightColorTemperatureKelvin ?? scene.LightColorTemperatureKelvin; var enableFragrance = customConfig?.EnableFragrance ?? scene.EnableFragrance; + var fragranceLevel = Math.Clamp(customConfig?.FragranceLevel ?? 1, 1, 3); + var fragranceDuration = fragranceLevel switch { 1 => 2, 2 => 4, _ => 6 }; + var fragranceInterval = fragranceLevel switch { 1 => 20, 2 => 15, _ => 10 }; var enableSpeakerSleepMode = customConfig?.EnableSpeakerSleepMode ?? scene.EnableSpeakerSleepMode; var speakerVolume = customConfig?.SpeakerVolumePercent ?? 15; var mattressHeatingLevel = customConfig?.MattressHeatingLevel ?? scene.MattressHeatingLevel; @@ -590,16 +750,43 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie if (enableFragrance) { await RunAction(states, results, "开启香氛机", entities.FragranceSwitch, "switch", "turn_on", null, cancellationToken); + await RunAction(states, results, "设置出香时长", entities.FragranceDuration, "number", "set_value", + new Dictionary { ["value"] = fragranceDuration }, cancellationToken); + await RunAction(states, results, "设置出香间隔", entities.FragranceInterval, "number", "set_value", + new Dictionary { ["value"] = fragranceInterval }, cancellationToken); await RunAction(states, results, "开启香氛灯", entities.FragranceLight, "light", "turn_on", new Dictionary { ["brightness_pct"] = lightBrightness }, cancellationToken); } - await RunAction(states, results, "开启助眠灯光", entities.AmbientLight, "light", "turn_on", - new Dictionary + if (string.IsNullOrWhiteSpace(entities.AmbientLight) || + !states.TryGetValue(entities.AmbientLight, out var lightState) || !IsAvailable(lightState)) + { + results.Add(new HomeAssistantActionResult { - ["brightness_pct"] = lightBrightness, - ["color_temp_kelvin"] = lightColorTemp - }, cancellationToken); + Name = "开启助眠渐暗灯光", EntityId = entities.AmbientLight, + Status = "Skipped", Message = "设备未配置或离线" + }); + } + else + { + try + { + await _lightTransition.StartSleepAsync(lightColorTemp, cancellationToken); + results.Add(new HomeAssistantActionResult + { + Name = "开启助眠渐暗灯光", EntityId = entities.AmbientLight, + Status = "Succeeded", Message = "已从50%开始,每5秒降低1%,最终关灯" + }); + } + catch (Exception exception) + { + results.Add(new HomeAssistantActionResult + { + Name = "开启助眠渐暗灯光", EntityId = entities.AmbientLight, + Status = "Failed", Message = exception.Message + }); + } + } if (enableSpeakerSleepMode) { @@ -635,6 +822,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie CancellationToken cancellationToken) { var entities = _options.Entities; + await _lightTransition.CancelAsync(cancellationToken); await RunAction(states, results, "关闭除湿机", entities.Dehumidifier, "humidifier", "turn_off", null, cancellationToken); await RunAction(states, results, "关闭香氛机", entities.FragranceSwitch, "switch", "turn_off", null, cancellationToken); await RunAction(states, results, "关闭香氛灯", entities.FragranceLight, "light", "turn_off", null, cancellationToken); @@ -835,7 +1023,8 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie AddFallback("fallback_dehumidifier", "米家变频净化除湿机 Max", "xiaomi.derh.60l", nameof(HealthDeviceCategory.EnvironmentControl), "睡眠空间", entities.Dehumidifier, entities.DehumidifierFanLevel, entities.DehumidifierUv); AddFallback("fallback_fragrance", "米家智能香氛机", "xiaomi.diffuser.xw002", nameof(HealthDeviceCategory.Fragrance), "睡眠空间", - entities.FragranceSwitch, entities.FragranceLight); + entities.FragranceSwitch, entities.FragranceLight, entities.FragranceDuration, entities.FragranceInterval, + entities.FragranceQuickRelease, entities.FragranceAutoRelease, entities.FragranceAutoLight, entities.FragranceLightAutoOff); AddFallback("fallback_ambient_light", "米家夜灯/氛围灯", "lemesh.light.wy0d02", nameof(HealthDeviceCategory.Lighting), "睡眠空间", entities.AmbientLight); AddFallback("fallback_speaker", "Xiaomi Sound Pro 智能音箱", "xiaomi.wifispeaker.l17a", nameof(HealthDeviceCategory.Media), "睡眠空间", @@ -1069,6 +1258,13 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie ["color_temp"] = _options.Entities.AmbientLight, ["volume"] = _options.Entities.Speaker, ["speaker_sleep_mode"] = _options.Entities.SpeakerSleepMode, + ["fragrance_duration"] = _options.Entities.FragranceDuration, + ["fragrance_interval"] = _options.Entities.FragranceInterval, + ["fragrance_light"] = _options.Entities.FragranceLight, + ["fragrance_quick_release"] = _options.Entities.FragranceQuickRelease, + ["fragrance_auto_release"] = _options.Entities.FragranceAutoRelease, + ["fragrance_auto_light"] = _options.Entities.FragranceAutoLight, + ["fragrance_light_auto_off"] = _options.Entities.FragranceLightAutoOff, ["mattress_left_heating"] = _options.Entities.MattressLeftHeatingLevel, ["mattress_right_heating"] = _options.Entities.MattressRightHeatingLevel, ["mattress_left_mode"] = _options.Entities.MattressLeftMode, @@ -1132,7 +1328,6 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie var dehumMode = userConfig?.DehumidifierMode ?? defaultScene.DehumidifierMode; var fanLevel = userConfig?.FanLevel ?? defaultScene.FanLevel; var enableUv = userConfig?.EnableUv ?? defaultScene.EnableUv; - var lightBrightness = userConfig?.LightBrightnessPercent ?? defaultScene.LightBrightnessPercent; var lightColorTemp = userConfig?.LightColorTemperatureKelvin ?? defaultScene.LightColorTemperatureKelvin; var enableFragrance = userConfig?.EnableFragrance ?? defaultScene.EnableFragrance; var enableSpeaker = userConfig?.EnableSpeakerSleepMode ?? defaultScene.EnableSpeakerSleepMode; @@ -1177,11 +1372,11 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie DeviceName = "Lemesh 色温灯", Category = nameof(HealthDeviceCategory.Lighting), EntityId = _options.Entities.AmbientLight, - Action = "开启暖光助眠", - Detail = $"亮度 {lightBrightness}% · 色温 {lightColorTemp}K", + Action = "开启助眠渐暗灯光", + Detail = $"从50%开始,每5秒降低1%,最终关灯 · 色温 {lightColorTemp}K", Parameters = new() { - ["brightness"] = $"{lightBrightness}%", + ["brightness"] = "50%→关闭", ["color_temp"] = $"{lightColorTemp}K" } });