diff --git a/Evergreen.Health.Application/Health/Models/HomeAssistantModels.cs b/Evergreen.Health.Application/Health/Models/HomeAssistantModels.cs index b82a66a..d5a3f62 100644 --- a/Evergreen.Health.Application/Health/Models/HomeAssistantModels.cs +++ b/Evergreen.Health.Application/Health/Models/HomeAssistantModels.cs @@ -109,6 +109,7 @@ public sealed class HomeAssistantDeviceSwitchOutput public string DeviceId { get; set; } = string.Empty; public string EntityId { get; set; } = string.Empty; public string State { get; set; } = string.Empty; + public bool? IsMuted { get; set; } public string Message { get; set; } = string.Empty; } diff --git a/Evergreen.Health.Application/Health/Services/HealthHomeAssistantService.cs b/Evergreen.Health.Application/Health/Services/HealthHomeAssistantService.cs index 9b888a2..4cea074 100644 --- a/Evergreen.Health.Application/Health/Services/HealthHomeAssistantService.cs +++ b/Evergreen.Health.Application/Health/Services/HealthHomeAssistantService.cs @@ -172,30 +172,86 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie }; } - var service = input.TurnOn ? "turn_on" : "turn_off"; + var targetOn = input.TurnOn; + bool? targetMuted = null; + var service = targetOn ? "turn_on" : "turn_off"; if (domain == "media_player") { - service = input.TurnOn ? "media_play" : "media_pause"; + // Xiaomi Sound Pro 的 media_player state 会长期保持 playing,不能用它 + // 判断声音是否开启;is_volume_muted 才是该实体可稳定回读的控制状态。 + var currentMutedText = currentState.GetStringAttribute("is_volume_muted"); + var currentMuted = bool.TryParse(currentMutedText, out var parsedMuted) && parsedMuted; + targetMuted = !currentMuted; + targetOn = !targetMuted.Value; + service = "volume_mute"; } var serviceData = new Dictionary { ["entity_id"] = targetEntityId }; + if (targetMuted.HasValue) + { + serviceData["is_volume_muted"] = targetMuted.Value; + } await _homeAssistant.CallServiceAsync(domain, service, serviceData, cancellationToken); + var resultState = domain == "media_player" + ? currentState.State + : (targetOn ? "on" : "off"); + bool? resultMuted = null; + + if (domain == "media_player") + { + var stateConfirmed = false; + for (var attempt = 0; attempt < 5; attempt++) + { + await Task.Delay(300, cancellationToken); + var refreshedStates = await _homeAssistant.GetStatesAsync(cancellationToken); + var refreshedState = refreshedStates.FirstOrDefault(s => + string.Equals(s.EntityId, targetEntityId, StringComparison.OrdinalIgnoreCase)); + if (refreshedState is null) + { + continue; + } + + resultState = refreshedState.State; + var refreshedMutedText = refreshedState.GetStringAttribute("is_volume_muted"); + resultMuted = bool.TryParse(refreshedMutedText, out var refreshedMuted) + ? refreshedMuted + : null; + stateConfirmed = resultMuted == targetMuted; + if (stateConfirmed) + { + break; + } + } + + if (!stateConfirmed) + { + return new HomeAssistantDeviceSwitchOutput + { + Success = false, + DeviceId = input.DeviceId, + EntityId = targetEntityId, + State = resultState, + IsMuted = resultMuted, + Message = $"音箱指令已发送,但静音状态未确认更新(当前:{(resultMuted.HasValue ? (resultMuted.Value ? "已静音" : "声音开启") : "未知")})" + }; + } + } + return new HomeAssistantDeviceSwitchOutput { Success = true, DeviceId = input.DeviceId, EntityId = targetEntityId, - State = domain == "media_player" - ? (input.TurnOn ? "playing" : "paused") - : (input.TurnOn ? "on" : "off"), + State = resultState, + IsMuted = resultMuted, Message = domain == "media_player" - ? $"已成功{(input.TurnOn ? "开始播放" : "暂停播放")}" - : $"已成功{(input.TurnOn ? "开启" : "关闭")}设备" + ? (targetMuted == true ? "音箱已静音" : "音箱声音已开启") + : $"已成功{(targetOn ? "开启" : "关闭")}设备" }; } catch (Exception ex) @@ -384,12 +440,21 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie { } + var successMessage = parameterType switch + { + "mattress_left_mode" when !string.Equals(serviceData["option"]?.ToString(), "无", StringComparison.Ordinal) => + "左侧承托模式已启动;床垫共用气泵,右侧会自动停止", + "mattress_right_mode" when !string.Equals(serviceData["option"]?.ToString(), "无", StringComparison.Ordinal) => + "右侧承托模式已启动;床垫共用气泵,左侧会自动停止", + _ => "参数调节成功" + }; + return new HomeAssistantSetParameterOutput { Success = true, DeviceId = input.DeviceId, ParameterType = input.ParameterType ?? "", - Message = "参数调节成功" + Message = successMessage }; } catch (Exception ex) @@ -539,13 +604,17 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie if (enableSpeakerSleepMode) { await RunAction(states, results, "开启音箱睡眠模式", entities.SpeakerSleepMode, "switch", "turn_on", null, cancellationToken); - await RunAction(states, results, "设置助眠音量", entities.Speaker, "media_player", "volume_set", - new Dictionary { ["volume_level"] = speakerVolume / 100d }, cancellationToken); } else { await RunAction(states, results, "关闭音箱睡眠模式", entities.SpeakerSleepMode, "switch", "turn_off", null, cancellationToken); } + // 音箱声音与睡眠模式是两种不同能力。即使用户未启用睡眠模式, + // 一键舒睡也应取消静音并应用个性化音量。 + await RunAction(states, results, "开启音箱声音", entities.Speaker, "media_player", "volume_mute", + new Dictionary { ["is_volume_muted"] = false }, cancellationToken); + await RunAction(states, results, "设置助眠音量", entities.Speaker, "media_player", "volume_set", + new Dictionary { ["volume_level"] = speakerVolume / 100d }, cancellationToken); if (scene.EnableMattress) { @@ -571,6 +640,8 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie await RunAction(states, results, "关闭香氛灯", entities.FragranceLight, "light", "turn_off", null, cancellationToken); await RunAction(states, results, "关闭助眠灯光", entities.AmbientLight, "light", "turn_off", null, cancellationToken); await RunAction(states, results, "关闭音箱睡眠模式", entities.SpeakerSleepMode, "switch", "turn_off", null, cancellationToken); + await RunAction(states, results, "静音音箱", entities.Speaker, "media_player", "volume_mute", + new Dictionary { ["is_volume_muted"] = true }, cancellationToken); if (_options.SleepScene.EnableMattress) { await RunAction(states, results, "关闭床垫左侧加热", entities.MattressLeftHeatingLevel, "number", "set_value", @@ -835,7 +906,9 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie sceneEntityIds.Add(entities.FragranceSwitch); if (!string.IsNullOrWhiteSpace(entities.AmbientLight)) sceneEntityIds.Add(entities.AmbientLight); - if (_options.SleepScene.EnableSpeakerSleepMode && !string.IsNullOrWhiteSpace(entities.SpeakerSleepMode)) + if (!string.IsNullOrWhiteSpace(entities.Speaker)) + sceneEntityIds.Add(entities.Speaker); + else if (_options.SleepScene.EnableSpeakerSleepMode && !string.IsNullOrWhiteSpace(entities.SpeakerSleepMode)) sceneEntityIds.Add(entities.SpeakerSleepMode); if (_options.SleepScene.EnableMattress && !string.IsNullOrWhiteSpace(entities.MattressLeftHeatingLevel)) sceneEntityIds.Add(entities.MattressLeftHeatingLevel); @@ -1114,17 +1187,17 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie }); } - if (enableSpeaker && !string.IsNullOrWhiteSpace(_options.Entities.SpeakerSleepMode)) + if (!string.IsNullOrWhiteSpace(_options.Entities.Speaker)) { var vol = userConfig?.SpeakerVolumePercent ?? 15; presets.Add(new HomeAssistantScenePresetItem { DeviceName = "Xiaomi Sound Pro", Category = nameof(HealthDeviceCategory.Media), - EntityId = _options.Entities.SpeakerSleepMode, - Action = "开启音箱睡眠模式", - Detail = $"切换睡眠模式开关 · 音量 {vol}%", - Parameters = new() { ["sleep_mode"] = "开启", ["volume"] = $"{vol}%" } + EntityId = _options.Entities.Speaker, + Action = enableSpeaker ? "开启音箱声音和睡眠模式" : "开启音箱声音", + Detail = enableSpeaker ? $"取消静音 · 开启睡眠模式 · 音量 {vol}%" : $"取消静音 · 音量 {vol}%", + Parameters = new() { ["muted"] = false, ["sleep_mode"] = enableSpeaker ? "开启" : "关闭", ["volume"] = $"{vol}%" } }); }