1111
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"TcmMusic": {
|
||||
"StartVolume": 60,
|
||||
"StartVolume": 50,
|
||||
"FadeStepSeconds": 5,
|
||||
"Tracks": {
|
||||
"balanced": [
|
||||
|
||||
+62
-2
@@ -155,7 +155,20 @@ public sealed class HomeAssistantLightTransitionService : IHostedService
|
||||
var light = await ReadAvailableLightAsync(cancellationToken);
|
||||
if (!string.Equals(light.State, "on", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
SetStatus(false, false, mode, 0, mode == "wake" ? WakeTargetPercent : 0, "灯已从其他位置关闭,渐变已停止");
|
||||
if (mode == "sleep")
|
||||
{
|
||||
try
|
||||
{
|
||||
await StopMattressMassageAsync(cancellationToken);
|
||||
SetStatus(true, false, mode, 0, 0, "灯已关闭,床垫按摩已停止");
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning(exception, "灯已关闭,但床垫按摩停止失败");
|
||||
SetStatus(false, false, mode, 0, 0, exception.Message);
|
||||
}
|
||||
}
|
||||
else SetStatus(false, false, mode, 0, WakeTargetPercent, "灯已从其他位置关闭,渐变已停止");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -178,7 +191,17 @@ public sealed class HomeAssistantLightTransitionService : IHostedService
|
||||
{
|
||||
["entity_id"] = LightEntityId
|
||||
}, cancellationToken);
|
||||
SetStatus(true, false, mode, 0, 0, "助眠渐暗已完成,灯光已关闭");
|
||||
SetStatus(true, false, mode, 0, 0, "灯光已关闭,正在停止床垫按摩");
|
||||
try
|
||||
{
|
||||
await StopMattressMassageAsync(cancellationToken);
|
||||
SetStatus(true, false, mode, 0, 0, "助眠渐暗已完成,灯光与床垫按摩已关闭");
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning(exception, "助眠渐暗已完成,但床垫按摩停止失败");
|
||||
SetStatus(false, false, mode, 0, 0, exception.Message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -204,6 +227,43 @@ public sealed class HomeAssistantLightTransitionService : IHostedService
|
||||
}
|
||||
}
|
||||
|
||||
private async Task StopMattressMassageAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var massageEntities = new[]
|
||||
{
|
||||
_options.Entities.MattressLeftMassagePart,
|
||||
_options.Entities.MattressRightMassagePart
|
||||
}.Where(entityId => !string.IsNullOrWhiteSpace(entityId)).Distinct().ToArray();
|
||||
var failures = new List<string>();
|
||||
foreach (var entityId in massageEntities)
|
||||
{
|
||||
try
|
||||
{
|
||||
var confirmed = false;
|
||||
for (var attempt = 0; attempt < 2 && !confirmed; attempt++)
|
||||
{
|
||||
await _homeAssistant.CallServiceAsync("select", "select_option", new Dictionary<string, object?>
|
||||
{
|
||||
["entity_id"] = entityId,
|
||||
["option"] = "无"
|
||||
}, cancellationToken);
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
var current = (await _homeAssistant.GetStatesAsync(cancellationToken))
|
||||
.FirstOrDefault(item => string.Equals(item.EntityId, entityId, StringComparison.OrdinalIgnoreCase));
|
||||
confirmed = current?.State == "无";
|
||||
}
|
||||
if (!confirmed) failures.Add(entityId);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning(exception, "灯光熄灭后未能关闭床垫按摩 {EntityId}", entityId);
|
||||
failures.Add(entityId);
|
||||
}
|
||||
}
|
||||
if (failures.Count > 0)
|
||||
throw new InvalidOperationException($"灯光已关闭,但床垫按摩未确认关闭:{string.Join("、", failures)}");
|
||||
}
|
||||
|
||||
private async Task StopCurrentAsync()
|
||||
{
|
||||
if (_activeCancellation is null) return;
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace Evergreen.Health.Application.Health.Integrations.MiAirX;
|
||||
|
||||
public sealed class TcmMusicOptions : IConfigurableOptions
|
||||
{
|
||||
public int StartVolume { get; set; } = 60;
|
||||
public int StartVolume { get; set; } = 50;
|
||||
public int FadeStepSeconds { get; set; } = 5;
|
||||
public Dictionary<string, List<TcmMusicTrackOptions>> Tracks { get; set; } = new();
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
private readonly TcmMusicOptions _options;
|
||||
private readonly ILogger<TcmMusicTransitionService> _logger;
|
||||
private readonly SemaphoreSlim _operationLock = new(1, 1);
|
||||
private readonly object _statusLock = new();
|
||||
private TcmMusicTransitionOutput _status = new();
|
||||
private CancellationTokenSource? _activeCancellation;
|
||||
private Task? _activeTask;
|
||||
|
||||
@@ -28,6 +30,18 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public TcmMusicTransitionOutput GetStatus()
|
||||
{
|
||||
lock (_statusLock)
|
||||
return new TcmMusicTransitionOutput
|
||||
{
|
||||
Running = _status.Running,
|
||||
HasVolume = _status.HasVolume,
|
||||
VolumePercent = _status.VolumePercent,
|
||||
Message = _status.Message
|
||||
};
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try { await StopMusicAsync(cancellationToken); }
|
||||
@@ -56,6 +70,7 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
await CancelCurrentLoopAsync();
|
||||
await _miAirX.SetVolumeAsync(_options.StartVolume, cancellationToken);
|
||||
await _miAirX.PlayUrlAsync(tracks[0].Url, cancellationToken);
|
||||
UpdateStatus(true, _options.StartVolume, "体质音乐正在自动渐弱");
|
||||
_activeCancellation = new CancellationTokenSource();
|
||||
_activeTask = RunFadeAsync(tracks, constitutionCode, _activeCancellation.Token);
|
||||
}
|
||||
@@ -69,6 +84,7 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
{
|
||||
await CancelCurrentLoopAsync();
|
||||
await _miAirX.PlayAsync(cancellationToken);
|
||||
UpdateStatus(false, null, "正在播放测试音乐");
|
||||
}
|
||||
finally { _operationLock.Release(); }
|
||||
}
|
||||
@@ -81,6 +97,7 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
// 手动调音量接管当前播放,避免下一次自动渐弱立刻覆盖用户选择。
|
||||
await CancelCurrentLoopAsync();
|
||||
await _miAirX.SetVolumeAsync(volume, cancellationToken);
|
||||
UpdateStatus(false, volume, "音量已手动设置");
|
||||
}
|
||||
finally { _operationLock.Release(); }
|
||||
}
|
||||
@@ -92,6 +109,7 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
{
|
||||
await CancelCurrentLoopAsync();
|
||||
await _miAirX.StopAsync(cancellationToken);
|
||||
UpdateStatus(false, null, "音乐已停止");
|
||||
}
|
||||
finally { _operationLock.Release(); }
|
||||
}
|
||||
@@ -116,12 +134,14 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
{
|
||||
currentVolume--;
|
||||
await _miAirX.SetVolumeAsync(currentVolume, cancellationToken);
|
||||
UpdateStatus(true, currentVolume, "体质音乐正在自动渐弱");
|
||||
nextFadeSecond += _options.FadeStepSeconds;
|
||||
}
|
||||
|
||||
if (currentVolume == 0)
|
||||
{
|
||||
await _miAirX.StopAsync(cancellationToken);
|
||||
UpdateStatus(false, 0, "音量已降至 0,音乐已停止");
|
||||
_logger.LogInformation("体质 {ConstitutionCode} 的助眠音乐已渐弱至 0 并停止", constitutionCode);
|
||||
return;
|
||||
}
|
||||
@@ -140,6 +160,7 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
_logger.LogWarning(exception, "体质音乐渐弱或续播中断");
|
||||
try { await _miAirX.StopAsync(CancellationToken.None); }
|
||||
catch (Exception stopException) { _logger.LogWarning(stopException, "体质音乐中断后停止音箱失败"); }
|
||||
UpdateStatus(false, null, "自动渐弱中断,请查看后端日志");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +174,21 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
cancellation.Cancel();
|
||||
try { if (task is not null) await task; }
|
||||
finally { cancellation.Dispose(); }
|
||||
UpdateStatus(false, null, "自动渐弱已停止");
|
||||
}
|
||||
|
||||
private void UpdateStatus(bool running, int? volume, string message)
|
||||
{
|
||||
lock (_statusLock)
|
||||
{
|
||||
_status.Running = running;
|
||||
if (volume.HasValue)
|
||||
{
|
||||
_status.HasVolume = true;
|
||||
_status.VolumePercent = volume.Value;
|
||||
}
|
||||
_status.Message = message;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsValidTrack(TcmMusicTrackOptions track)
|
||||
@@ -160,3 +196,11 @@ public sealed class TcmMusicTransitionService : IHostedService
|
||||
Uri.TryCreate(track.Url, UriKind.Absolute, out var uri) &&
|
||||
uri.Scheme is "http" or "https";
|
||||
}
|
||||
|
||||
public sealed class TcmMusicTransitionOutput
|
||||
{
|
||||
public bool Running { get; set; }
|
||||
public bool HasVolume { get; set; }
|
||||
public int VolumePercent { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
@@ -160,7 +160,7 @@ public sealed class PersonalizedSceneConfig
|
||||
|
||||
public bool EnableSpeakerSleepMode { get; set; } = true;
|
||||
public string SpeakerAudioMode { get; set; } = "助眠白噪音";
|
||||
public int SpeakerVolumePercent { get; set; } = 15;
|
||||
public int SpeakerVolumePercent { get; set; } = 50;
|
||||
}
|
||||
|
||||
public sealed class HealthAssessmentAnswers
|
||||
|
||||
@@ -6,6 +6,7 @@ using Furion.DatabaseAccessor;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using SqlSugar;
|
||||
using System.Globalization;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
|
||||
@@ -51,6 +52,10 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
||||
[HttpGet("/api/health/home-assistant/lights/transition")]
|
||||
public HomeAssistantLightTransitionOutput GetLightTransitionStatus() => _lightTransition.GetStatus();
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("/api/health/home-assistant/music/transition")]
|
||||
public TcmMusicTransitionOutput GetMusicTransitionStatus() => _musicTransition.GetStatus();
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("/api/health/home-assistant/lights/sleep")]
|
||||
public async Task<HomeAssistantLightTransitionOutput> StartSleepLight(CancellationToken cancellationToken)
|
||||
@@ -500,6 +505,9 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
||||
|
||||
serviceData["entity_id"] = targetEntityId;
|
||||
await _homeAssistant.CallServiceAsync(domain, service, serviceData, cancellationToken);
|
||||
if (parameterType is "mattress_left_mode" or "mattress_right_mode")
|
||||
await VerifyMattressModeAsync(targetEntityId, serviceData["option"]?.ToString() ?? "", serviceData,
|
||||
cancellationToken);
|
||||
|
||||
var successMessage = parameterType switch
|
||||
{
|
||||
@@ -584,12 +592,14 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
||||
var successCount = actions.Count(item => item.Status == "Succeeded");
|
||||
var failedCount = actions.Count(item => item.Status == "Failed");
|
||||
var skippedCount = actions.Count(item => item.Status == "Skipped");
|
||||
var failedNames = string.Join("、", actions.Where(item => item.Status == "Failed")
|
||||
.Select(item => item.Name).Distinct().Take(3));
|
||||
status = failedCount == 0 && successCount > 0 ? "Succeeded" : successCount > 0 ? "Partial" : "Failed";
|
||||
message = status switch
|
||||
{
|
||||
"Succeeded" when skippedCount > 0 => $"场景已执行,{successCount} 项成功,{skippedCount} 项因设备离线或未配置而跳过",
|
||||
"Succeeded" => $"场景已执行,{successCount} 项成功",
|
||||
"Partial" => $"场景部分执行成功:{successCount} 项成功,{failedCount} 项失败,{skippedCount} 项跳过",
|
||||
"Partial" => $"场景部分执行成功:{successCount} 项成功,{failedCount} 项失败,{skippedCount} 项跳过;未完成:{failedNames}",
|
||||
_ => "当前没有可执行的在线设备"
|
||||
};
|
||||
}
|
||||
@@ -723,7 +733,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
||||
new Dictionary<string, object?> { ["value"] = mattressHeatingLevel }, cancellationToken);
|
||||
await RunAction(states, results, "设置床垫右侧加热档位", entities.MattressRightHeatingLevel, "number", "set_value",
|
||||
new Dictionary<string, object?> { ["value"] = mattressHeatingLevel }, cancellationToken);
|
||||
// 左右承托模式共用气泵,一键场景不同时启动两侧模式。
|
||||
// 左右承托模式共用气泵;一键场景只实际启动左侧,右侧留作页面预选。
|
||||
if (mattressFirmness == null)
|
||||
{
|
||||
results.Add(new HomeAssistantActionResult
|
||||
@@ -741,6 +751,25 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
||||
new Dictionary<string, object?> { ["option"] = mattressFirmness }, cancellationToken);
|
||||
}
|
||||
}
|
||||
const string leftModeActionName = "开启床垫左侧智能适配";
|
||||
await RunAction(states, results, leftModeActionName, entities.MattressLeftMode, "select", "select_option",
|
||||
new Dictionary<string, object?> { ["option"] = "智能适配" }, cancellationToken);
|
||||
var leftModeAction = results.LastOrDefault(item => item.Name == leftModeActionName);
|
||||
if (leftModeAction?.Status == "Succeeded")
|
||||
{
|
||||
try
|
||||
{
|
||||
await VerifyMattressModeAsync(entities.MattressLeftMode, "智能适配",
|
||||
new Dictionary<string, object?> { ["entity_id"] = entities.MattressLeftMode, ["option"] = "智能适配" },
|
||||
cancellationToken);
|
||||
leftModeAction.Message = "已确认左侧智能适配运行,右侧保持待机预选";
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
leftModeAction.Status = "Failed";
|
||||
leftModeAction.Message = exception.Message;
|
||||
}
|
||||
}
|
||||
// 软硬度调节可能占用床垫气泵;等这些指令完成后,最后启动按摩。
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
await RunAction(states, results, "停止床垫左侧按摩", entities.MattressLeftMassagePart, "select", "select_option",
|
||||
@@ -787,21 +816,119 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
||||
{
|
||||
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);
|
||||
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.MattressRightMassagePart, "select", "select_option",
|
||||
new Dictionary<string, object?> { ["option"] = "无" }, cancellationToken);
|
||||
await RunAction(states, results, "关闭床垫左侧按摩", entities.MattressLeftMassagePart, "select", "select_option",
|
||||
new Dictionary<string, object?> { ["option"] = "无" }, cancellationToken);
|
||||
var commands = new List<OffCommand>
|
||||
{
|
||||
new("关闭除湿机", entities.Dehumidifier, "humidifier", "turn_off", "off", null),
|
||||
new("关闭香氛机", entities.FragranceSwitch, "switch", "turn_off", "off", null),
|
||||
new("关闭香氛灯", entities.FragranceLight, "light", "turn_off", "off", null),
|
||||
new("关闭感应夜灯", entities.FragranceAutoLight, "switch", "turn_off", "off", null),
|
||||
new("关闭人来增香", entities.FragranceAutoRelease, "switch", "turn_off", "off", null),
|
||||
new("关闭助眠灯光", entities.AmbientLight, "light", "turn_off", "off", null),
|
||||
new("关闭音箱睡眠模式", entities.SpeakerSleepMode, "switch", "turn_off", "off", null),
|
||||
new("关闭床垫右侧按摩", entities.MattressRightMassagePart, "select", "select_option", "无",
|
||||
new Dictionary<string, object?> { ["option"] = "无" }),
|
||||
new("关闭床垫左侧按摩", entities.MattressLeftMassagePart, "select", "select_option", "无",
|
||||
new Dictionary<string, object?> { ["option"] = "无" })
|
||||
};
|
||||
if (_options.SleepScene.EnableMattress)
|
||||
{
|
||||
await RunAction(states, results, "关闭床垫左侧加热", entities.MattressLeftHeatingLevel, "number", "set_value",
|
||||
new Dictionary<string, object?> { ["value"] = 0 }, cancellationToken);
|
||||
await RunAction(states, results, "关闭床垫右侧加热", entities.MattressRightHeatingLevel, "number", "set_value",
|
||||
new Dictionary<string, object?> { ["value"] = 0 }, cancellationToken);
|
||||
commands.Add(new("关闭床垫左侧加热", entities.MattressLeftHeatingLevel, "number", "set_value", "0",
|
||||
new Dictionary<string, object?> { ["value"] = 0 }));
|
||||
commands.Add(new("关闭床垫右侧加热", entities.MattressRightHeatingLevel, "number", "set_value", "0",
|
||||
new Dictionary<string, object?> { ["value"] = 0 }));
|
||||
}
|
||||
|
||||
foreach (var command in commands)
|
||||
await RunAction(states, results, command.Name, command.EntityId, command.Domain, command.Service,
|
||||
command.ExtraData, cancellationToken);
|
||||
await VerifyOffActionsAsync(commands, results, cancellationToken);
|
||||
}
|
||||
|
||||
private sealed record OffCommand(string Name, string EntityId, string Domain, string Service,
|
||||
string ExpectedState, IReadOnlyDictionary<string, object?>? ExtraData);
|
||||
|
||||
private async Task VerifyOffActionsAsync(
|
||||
IReadOnlyList<OffCommand> commands,
|
||||
List<HomeAssistantActionResult> results,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var pending = commands.Where(item => !string.IsNullOrWhiteSpace(item.EntityId)).ToList();
|
||||
for (var round = 0; round < 3 && pending.Count > 0; round++)
|
||||
{
|
||||
// HA 接受指令并不代表设备已变更,尤其是云端控制的香氛机。
|
||||
await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken);
|
||||
IReadOnlyDictionary<string, HomeAssistantState> freshStates;
|
||||
try
|
||||
{
|
||||
freshStates = (await _homeAssistant.GetStatesAsync(cancellationToken))
|
||||
.ToDictionary(item => item.EntityId, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
foreach (var command in pending)
|
||||
{
|
||||
var action = results.Last(item => item.Name == command.Name);
|
||||
action.Status = "Failed";
|
||||
action.Message = $"关闭指令已发送,但无法回读确认:{exception.Message}";
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var command in pending.ToArray())
|
||||
{
|
||||
var action = results.Last(item => item.Name == command.Name);
|
||||
freshStates.TryGetValue(command.EntityId, out var state);
|
||||
if (state is not null && IsOffState(state.State, command.ExpectedState))
|
||||
{
|
||||
action.Status = "Succeeded";
|
||||
action.Message = round == 0 ? "已回读确认关闭" : "重试后已回读确认关闭";
|
||||
pending.Remove(command);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (round == 2)
|
||||
{
|
||||
action.Status = "Failed";
|
||||
action.Message = $"关闭后仍未确认目标状态(当前:{state?.State ?? "未读取到"})";
|
||||
pending.Remove(command);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (state is null || !IsAvailable(state)) continue;
|
||||
try
|
||||
{
|
||||
var data = new Dictionary<string, object?> { ["entity_id"] = command.EntityId };
|
||||
if (command.ExtraData is not null)
|
||||
foreach (var item in command.ExtraData) data[item.Key] = item.Value;
|
||||
await _homeAssistant.CallServiceAsync(command.Domain, command.Service, data, cancellationToken);
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
action.Status = "Failed";
|
||||
action.Message = $"重试关闭失败:{exception.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsOffState(string actual, string expected)
|
||||
=> expected == "0"
|
||||
? decimal.TryParse(actual, NumberStyles.Any, CultureInfo.InvariantCulture, out var value) && value == 0
|
||||
: string.Equals(actual, expected, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private async Task VerifyMattressModeAsync(string entityId, string expected,
|
||||
IReadOnlyDictionary<string, object?> serviceData, CancellationToken cancellationToken)
|
||||
{
|
||||
for (var attempt = 0; attempt < 3; attempt++)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken);
|
||||
var actual = (await _homeAssistant.GetStatesAsync(cancellationToken))
|
||||
.FirstOrDefault(item => string.Equals(item.EntityId, entityId, StringComparison.OrdinalIgnoreCase))?.State;
|
||||
if (string.Equals(actual, expected, StringComparison.Ordinal)) return;
|
||||
if (attempt < 2)
|
||||
await _homeAssistant.CallServiceAsync("select", "select_option", serviceData, cancellationToken);
|
||||
else
|
||||
throw new InvalidOperationException($"床垫承托模式未确认切换到{expected}(当前:{actual ?? "未读取到"})");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1423,8 +1550,8 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
||||
Category = nameof(HealthDeviceCategory.Media),
|
||||
EntityId = _options.Entities.Speaker,
|
||||
Action = enableSpeaker ? "播放体质音乐并开启睡眠模式" : "播放体质音乐",
|
||||
Detail = enableSpeaker ? "体质曲目顺序播放 · 音量60%每5秒降1% · 开启睡眠模式" : "体质曲目顺序播放 · 音量60%每5秒降1%",
|
||||
Parameters = new() { ["music"] = "四诊体质专属曲目", ["sleep_mode"] = enableSpeaker ? "开启" : "关闭", ["volume"] = "60%→0%" }
|
||||
Detail = enableSpeaker ? "体质曲目循环播放 · 音量50%每5秒降1% · 开启睡眠模式" : "体质曲目循环播放 · 音量50%每5秒降1%",
|
||||
Parameters = new() { ["music"] = "四诊体质专属曲目", ["sleep_mode"] = enableSpeaker ? "开启" : "关闭", ["volume"] = "50%→0%" }
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,12 @@ public static class TcmConstitutionRules
|
||||
try
|
||||
{
|
||||
var result = JsonSerializer.Deserialize<TcmAssessmentResult>(profile.TcmResultJson) ?? new TcmAssessmentResult();
|
||||
if (result.HasCompleted) result.MusicRecommendation = MusicFor(result.ConstitutionCode);
|
||||
if (result.HasCompleted)
|
||||
{
|
||||
result.MusicRecommendation = MusicFor(result.ConstitutionCode);
|
||||
result.SceneConfig ??= new PersonalizedSceneConfig();
|
||||
result.SceneConfig.SpeakerVolumePercent = 50;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
catch { return new TcmAssessmentResult(); }
|
||||
@@ -209,7 +214,7 @@ public static class TcmConstitutionRules
|
||||
MattressHeatingLevel = 0, MattressLeftFirmness = 0, MattressRightFirmness = 0,
|
||||
MattressMode = "智能适配",
|
||||
EnableFragrance = fragrance && input.Answers["allergy"] != "often", FragranceLevel = 1,
|
||||
EnableSpeakerSleepMode = true, SpeakerVolumePercent = 60
|
||||
EnableSpeakerSleepMode = true, SpeakerVolumePercent = 50
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
当前 MiAirX 管理页已开启密码保护,在启动后端的环境中设置 `MiAirX__WebPassword` 环境变量(Visual Studio 可在本机启动配置中设置)。不要把管理密码写入 JSON 或提交到仓库。若以后关闭了 MiAirX 的密码保护,可将 `RequireWebPassword` 改为 `false`。后端需要能访问 MiAirX 服务地址;首次联调建议先在设备页手动测试“播放音乐”和“停止播放”,再测试一键启动/关闭。MiAirX 返回成功表示指令已送达,不代表已经确认音箱实际发声。
|
||||
|
||||
九体质演示曲目在 `Evergreen.Health.Application/Configuration/TcmMusic.json` 中按体质代码排列,当前 9 首的 OSS URL 已配置。每个体质的列表可按顺序增加多首;一键开启从 60% 音量开始,每 5 秒降低 1%,单曲结束自动播下一首并继承当前音量,末曲结束后从头循环(只有一首时重播该曲),直到音量为 0 时停止。当前按配置的曲目时长切换,MiAirX 并未提供此播放方式的可靠结束事件。手动播放按钮仍使用 `MiAirX.json` 的测试 URL;手动调整音量会停止自动渐弱,但不会停止当前音乐。
|
||||
九体质演示曲目在 `Evergreen.Health.Application/Configuration/TcmMusic.json` 中按体质代码排列,当前 9 首的 OSS URL 已配置。每个体质的列表可按顺序增加多首;一键开启从 50% 音量开始,每 5 秒降低 1%,单曲结束自动播下一首并继承当前音量,末曲结束后从头循环(只有一首时重播该曲),直到音量为 0 时停止。页面显示的是 MiAirX 已成功下发的音量,不依赖 Home Assistant 中可能延迟的音量状态。当前按配置的曲目时长切换,MiAirX 并未提供此播放方式的可靠结束事件。手动播放按钮仍使用 `MiAirX.json` 的测试 URL;手动调整音量会停止自动渐弱,但不会停止当前音乐。
|
||||
|
||||
## 健康业务接口
|
||||
|
||||
|
||||
Reference in New Issue
Block a user