This commit is contained in:
fuenmao
2026-10-09 16:46:30 +08:00
parent 4a634ef4c2
commit e6b90a06ab
63 changed files with 4170 additions and 1518 deletions
@@ -0,0 +1,120 @@
using Evergreen.Health.Application.Health.Devices.Contracts;
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
using Evergreen.Health.Application.Health.Models;
using Evergreen.Health.Application.Health.Scenes;
using Microsoft.Extensions.Options;
using Evergreen.Health.Application.Health.Services;
namespace Evergreen.Health.Application.Health.Devices.Mattress;
public sealed class MattressModule : HealthDeviceModule, ILightOffHandler
{
private readonly HomeAssistantOptions _options;
private HomeAssistantEntityOptions Entities => _options.Entities;
public MattressModule(DeviceActionRunner runner, IOptions<HomeAssistantOptions> options) : base(runner)
{
_options = options.Value;
var modes = new[] { "无", "瑜伽助眠", "智能适配", "智能腰托" };
var parts = new[] { "无", "背部", "腰部", "牵引", "腿部", "全身" };
var parameters = new List<DeviceParameterDefinition>
{
DeviceValues.Number("mattress_left_heating", Entities.MattressLeftHeatingLevel, 0, 4, "床垫左侧加热档位"),
DeviceValues.Number("mattress_right_heating", Entities.MattressRightHeatingLevel, 0, 4, "床垫右侧加热档位"),
DeviceValues.Select("mattress_left_mode", Entities.MattressLeftMode, modes, "床垫左侧承托模式", true),
DeviceValues.Select("mattress_right_mode", Entities.MattressRightMode, modes, "床垫右侧承托模式", true),
DeviceValues.Select("mattress_massage_left_part", Entities.MattressLeftMassagePart, parts, "左侧按摩部位",
intent: DeviceCommandIntent.StartMassage),
DeviceValues.Select("mattress_massage_right_part", Entities.MattressRightMassagePart, parts, "右侧按摩部位",
intent: DeviceCommandIntent.StartMassage),
DeviceValues.Select("mattress_massage_level", Entities.MattressMassageLevel, ["轻柔", "强劲"], "按摩力度")
};
foreach (var (zone, entityId) in Entities.MattressFirmness)
parameters.Add(DeviceValues.Select($"mattress_firmness_{zone}", entityId,
["最软", "很软", "稍软", "稍硬", "很硬", "最硬"], $"床垫{ZoneName(zone)}软硬度"));
Parameters = parameters;
}
public override string Key => "mattress";
public override string Category => nameof(HealthDeviceCategory.Sleep);
public override string DisplayName => "智能床垫";
public override string PrimaryEntityId => ""; // 床垫没有单一总开关,不能把加热实体冒充开关。
public override IReadOnlyList<DeviceParameterDefinition> Parameters { get; }
public override async Task ExecuteSleepAsync(DeviceSceneContext context, PersonalizedSceneConfig config,
string constitutionCode, CancellationToken cancellationToken)
{
if (!_options.SleepScene.EnableMattress) return;
await context.RunAsync(BuildParameterCommand("mattress_left_heating", config.MattressHeatingLevel), cancellationToken);
await context.RunAsync(BuildParameterCommand("mattress_right_heating", config.MattressHeatingLevel), cancellationToken);
var firmness = MattressComfortService.FirmnessName(config.MattressLeftFirmness);
if (firmness is null)
context.Results.Add(new() { Name = "按 BMI 调节床垫软硬度", Status = "Skipped",
Message = "健康档案缺少身高或体重,未自动修改软硬度" });
else
foreach (var zone in Entities.MattressFirmness.Keys)
await context.RunAsync(BuildParameterCommand($"mattress_firmness_{zone}", firmness), cancellationToken);
var modeCommand = BuildParameterCommand("mattress_left_mode", "智能适配");
await context.RunAsync(modeCommand, cancellationToken);
if (context.Results.Last().Status == "Succeeded")
await Runner.VerifyBatchAsync([modeCommand], context.Results, cancellationToken);
// 气泵调节结束后再启动按摩,保持真实设备所需的顺序。
await Runner.DelayAsync(TimeSpan.FromSeconds(2), cancellationToken);
await context.RunAsync(BuildParameterCommand("mattress_massage_left_part", "无"), cancellationToken);
await context.RunAsync(BuildParameterCommand("mattress_massage_level", "轻柔"), cancellationToken);
var massage = BuildParameterCommand("mattress_massage_right_part", "全身") with { ExpectedState = "全身" };
await context.RunAsync(massage, cancellationToken);
if (context.Results.Last().Status == "Succeeded")
{
await Runner.DelayAsync(TimeSpan.FromSeconds(3), cancellationToken);
await Runner.VerifyBatchAsync([massage], context.Results, cancellationToken);
}
}
public IEnumerable<DeviceCommand> MassageOffCommands() =>
[
new("关闭床垫右侧按摩", Entities.MattressRightMassagePart, "select", "select_option",
new Dictionary<string, object?> { ["option"] = "无" }, "无"),
new("关闭床垫左侧按摩", Entities.MattressLeftMassagePart, "select", "select_option",
new Dictionary<string, object?> { ["option"] = "无" }, "无")
];
public override IEnumerable<DeviceCommand> GetShutdownCommands()
{
foreach (var command in MassageOffCommands()) yield return command;
// 未参与自动启动不等于无需关闭:手动开启的加热也要随一键关闭停止。
yield return new("关闭床垫左侧加热", Entities.MattressLeftHeatingLevel, "number", "set_value",
new Dictionary<string, object?> { ["value"] = 0 }, "0");
yield return new("关闭床垫右侧加热", Entities.MattressRightHeatingLevel, "number", "set_value",
new Dictionary<string, object?> { ["value"] = 0 }, "0");
}
public async Task HandleAsync(CancellationToken cancellationToken)
{
var commands = MassageOffCommands().Where(command => !string.IsNullOrWhiteSpace(command.EntityId)).ToArray();
var results = new List<HomeAssistantActionResult>();
foreach (var command in commands)
await Runner.RunAsync(new Dictionary<string, HomeAssistantState>(), results, command, cancellationToken, false);
await Runner.VerifyBatchAsync(commands, results, cancellationToken);
if (results.Any(result => result.Status == "Failed"))
throw new InvalidOperationException("床垫按摩未确认关闭:" +
string.Join(";", results.Where(result => result.Status == "Failed").Select(result => result.Message)));
}
public override HomeAssistantScenePresetItem? BuildPreview(PersonalizedSceneConfig config, string constitutionCode)
{
if (!_options.SleepScene.EnableMattress || !EntityIds.Any()) return null;
var firmness = MattressComfortService.FirmnessName(config.MattressLeftFirmness) ?? "未自动调节";
var preview = Preview("按室温和 BMI 调节床垫并开启右侧按摩",
$"左右加热 {config.MattressHeatingLevel} 档 · 软硬度{firmness} · 右侧全身轻柔按摩",
new() { ["heating_level"] = config.MattressHeatingLevel, ["firmness"] = firmness,
["right_massage_part"] = "全身", ["massage_level"] = "轻柔" });
preview.EntityId = EntityIds.First();
return preview;
}
public static string ZoneName(string zone) => zone switch
{
"left_shoulder" => "左肩", "right_shoulder" => "右肩", "left_back" => "左背", "right_back" => "右背",
"left_waist" => "左腰", "right_waist" => "右腰", "left_lumbar" => "左腰椎", "right_lumbar" => "右腰椎",
"left_hip" => "左臀", "right_hip" => "右臀", "left_leg" => "左腿", "right_leg" => "右腿", _ => zone
};
}