This commit is contained in:
fuenmao
2026-09-22 15:01:29 +08:00
parent a063be61cd
commit c1c86b56b6
5 changed files with 797 additions and 45 deletions
@@ -62,6 +62,12 @@ public sealed class HealthProfile : EntityBase
[SugarColumn(ColumnDescription = "健康目标", Length = 500, IsNullable = true)]
public string? HealthGoal { get; set; }
[SugarColumn(ColumnDescription = "健康评估问卷答案JSON", ColumnDataType = "text", IsNullable = true)]
public string? AssessmentJson { get; set; }
[SugarColumn(ColumnDescription = "个性化智能设备启动参数JSON", ColumnDataType = "text", IsNullable = true)]
public string? CustomSceneConfigJson { get; set; }
}
[SugarTable("health_module_selection", "用户健康板块选择")]
@@ -94,4 +94,66 @@ public sealed class HealthDashboardOutput
public SaveHealthProfileInput? Profile { get; set; }
public List<string> SelectedModules { get; set; } = [];
public HealthPlanOutput? CurrentPlan { get; set; }
public bool HasCompletedAssessment { get; set; }
public decimal? Bmi { get; set; }
public string BmiStatus { get; set; } = string.Empty;
public PersonalizedSceneConfig? SceneConfig { get; set; }
}
public sealed class PersonalizedSceneConfig
{
public int TargetHumidity { get; set; } = 55;
public string DehumidifierMode { get; set; } = "睡眠";
public string FanLevel { get; set; } = "1档";
public bool EnableUv { get; set; } = false;
public int LightBrightnessPercent { get; set; } = 20;
public int LightColorTemperatureKelvin { get; set; } = 2700;
public decimal MattressTargetTemp { get; set; } = 36m;
public int MattressLeftFirmness { get; set; } = 5;
public int MattressRightFirmness { get; set; } = 6;
public int MattressHeatingLevel { get; set; } = 1;
public string MattressMode { get; set; } = "智能适配";
public bool EnableFragrance { get; set; } = true;
public int FragranceLevel { get; set; } = 1;
public bool EnableSpeakerSleepMode { get; set; } = true;
public string SpeakerAudioMode { get; set; } = "助眠白噪音";
public int SpeakerVolumePercent { get; set; } = 15;
}
public sealed class HealthAssessmentAnswers
{
public string SleepDuration { get; set; } = string.Empty;
public string FallingAsleepSpeed { get; set; } = string.Empty;
public string TemperaturePreference { get; set; } = string.Empty;
public string NightAwakening { get; set; } = string.Empty;
public string MattressPreference { get; set; } = string.Empty;
public string ExerciseFrequency { get; set; } = string.Empty;
public string MuscleFatigueArea { get; set; } = string.Empty;
public string ActiveTimeOfDay { get; set; } = string.Empty;
public string MealRegularity { get; set; } = string.Empty;
public string FlavorPreference { get; set; } = string.Empty;
public string LateNightEating { get; set; } = string.Empty;
public string DailyWaterIntake { get; set; } = string.Empty;
}
public sealed class SaveHealthAssessmentInput
{
public SaveHealthProfileInput BasicInfo { get; set; } = new();
public HealthAssessmentAnswers Answers { get; set; } = new();
}
public sealed class HealthAssessmentOutput
{
public bool HasCompletedAssessment { get; set; }
public SaveHealthProfileInput BasicInfo { get; set; } = new();
public HealthAssessmentAnswers Answers { get; set; } = new();
public PersonalizedSceneConfig SceneConfig { get; set; } = new();
public decimal? Bmi { get; set; }
public string BmiStatus { get; set; } = "正常";
}
@@ -73,6 +73,15 @@ public sealed class HomeAssistantStatusOutput
public HomeAssistantSceneDevicesSummaryOutput SceneDevices { get; set; } = new();
public HomeAssistantEnvironmentOutput Environment { get; set; } = new();
public List<HomeAssistantScenePresetItem> ScenePresets { get; set; } = [];
public Dictionary<string, HomeAssistantParameterStateOutput> ControlParameters { get; set; } = [];
}
public sealed class HomeAssistantParameterStateOutput
{
public string EntityId { get; set; } = string.Empty;
public string State { get; set; } = string.Empty;
public bool Available { get; set; }
public Dictionary<string, object?>? Attributes { get; set; }
}
public sealed class HomeAssistantScenePresetItem
@@ -103,6 +112,22 @@ public sealed class HomeAssistantDeviceSwitchOutput
public string Message { get; set; } = string.Empty;
}
public sealed class HomeAssistantSetParameterInput
{
public string DeviceId { get; set; } = string.Empty;
public string? EntityId { get; set; }
public string ParameterType { get; set; } = string.Empty;
public object? Value { get; set; }
}
public sealed class HomeAssistantSetParameterOutput
{
public bool Success { get; set; }
public string DeviceId { get; set; } = string.Empty;
public string ParameterType { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
public sealed class HomeAssistantSceneOutput
{
public string SceneCode { get; set; } = string.Empty;
@@ -104,15 +104,151 @@ public sealed class HealthAppService : IDynamicApiController, ITransient
.OrderByDescending(item => item.Version)
.FirstAsync();
var hasCompletedAssessment = profile != null && !string.IsNullOrWhiteSpace(profile.AssessmentJson);
decimal? bmi = null;
var bmiStatus = string.Empty;
if (profile?.HeightCm > 0 && profile.WeightKg > 0)
{
var h = profile.HeightCm.Value / 100m;
bmi = Math.Round(profile.WeightKg.Value / (h * h), 1);
bmiStatus = bmi < 18.5m ? "偏瘦" : bmi <= 23.9m ? "正常" : bmi <= 27.9m ? "超重" : "肥胖";
}
PersonalizedSceneConfig? sceneConfig = null;
if (!string.IsNullOrWhiteSpace(profile?.CustomSceneConfigJson))
{
try
{
sceneConfig = JsonSerializer.Deserialize<PersonalizedSceneConfig>(profile.CustomSceneConfigJson);
}
catch
{
}
}
if (sceneConfig == null && hasCompletedAssessment)
{
var answers = JsonSerializer.Deserialize<HealthAssessmentAnswers>(profile!.AssessmentJson!) ?? new HealthAssessmentAnswers();
sceneConfig = CalculatePersonalizedSceneConfig(answers, MapProfile(profile));
}
return new HealthDashboardOutput
{
User = MapUser(user),
Profile = profile is null ? null : MapProfile(profile),
SelectedModules = modules,
CurrentPlan = plan is null ? null : MapPlan(plan)
CurrentPlan = plan is null ? null : MapPlan(plan),
HasCompletedAssessment = hasCompletedAssessment,
Bmi = bmi,
BmiStatus = bmiStatus,
SceneConfig = sceneConfig
};
}
[Authorize]
[HttpGet("/api/health/assessment")]
public async Task<HealthAssessmentOutput> GetAssessment()
{
var userId = GetCurrentUserId();
var profile = await _profileRepository.GetFirstAsync(p => p.UserId == userId);
if (profile == null)
{
return new HealthAssessmentOutput
{
HasCompletedAssessment = false,
BasicInfo = new SaveHealthProfileInput(),
Answers = new HealthAssessmentAnswers(),
SceneConfig = new PersonalizedSceneConfig()
};
}
var answers = string.IsNullOrWhiteSpace(profile.AssessmentJson)
? new HealthAssessmentAnswers()
: JsonSerializer.Deserialize<HealthAssessmentAnswers>(profile.AssessmentJson) ?? new HealthAssessmentAnswers();
var hasCompleted = !string.IsNullOrWhiteSpace(profile.AssessmentJson);
var sceneConfig = string.IsNullOrWhiteSpace(profile.CustomSceneConfigJson)
? CalculatePersonalizedSceneConfig(answers, MapProfile(profile))
: JsonSerializer.Deserialize<PersonalizedSceneConfig>(profile.CustomSceneConfigJson) ?? new PersonalizedSceneConfig();
decimal? bmi = null;
var bmiStatus = "正常";
if (profile.HeightCm > 0 && profile.WeightKg > 0)
{
var h = profile.HeightCm.Value / 100m;
bmi = Math.Round(profile.WeightKg.Value / (h * h), 1);
bmiStatus = bmi < 18.5m ? "偏瘦" : bmi <= 23.9m ? "正常" : bmi <= 27.9m ? "超重" : "肥胖";
}
return new HealthAssessmentOutput
{
HasCompletedAssessment = hasCompleted,
BasicInfo = MapProfile(profile),
Answers = answers,
SceneConfig = sceneConfig,
Bmi = bmi,
BmiStatus = bmiStatus
};
}
[Authorize]
[HttpPost("/api/health/assessment")]
[UnitOfWork]
public async Task<HealthAssessmentOutput> SaveAssessment(SaveHealthAssessmentInput input)
{
ValidateAssessment(input);
var userId = GetCurrentUserId();
var profile = await _profileRepository.GetFirstAsync(p => p.UserId == userId);
var calculatedConfig = CalculatePersonalizedSceneConfig(input.Answers, input.BasicInfo);
if (profile == null)
{
profile = new HealthProfile { UserId = userId };
ApplyProfile(profile, input.BasicInfo);
profile.AssessmentJson = JsonSerializer.Serialize(input.Answers);
profile.CustomSceneConfigJson = JsonSerializer.Serialize(calculatedConfig);
await _profileRepository.InsertAsync(profile);
}
else
{
ApplyProfile(profile, input.BasicInfo);
profile.AssessmentJson = JsonSerializer.Serialize(input.Answers);
profile.CustomSceneConfigJson = JsonSerializer.Serialize(calculatedConfig);
await _profileRepository.UpdateAsync(profile);
}
var modules = new List<string> { "sleep", "exercise", "diet" };
var existingModules = await _moduleRepository.AsQueryable().Where(m => m.UserId == userId).ToListAsync();
foreach (var mod in modules)
{
var existing = existingModules.FirstOrDefault(m => m.ModuleCode == mod);
if (existing == null)
{
await _moduleRepository.InsertAsync(new HealthModuleSelection { UserId = userId, ModuleCode = mod, IsEnabled = true });
}
else if (!existing.IsEnabled)
{
existing.IsEnabled = true;
await _moduleRepository.UpdateAsync(existing);
}
}
var maxVersion = await _planRepository.AsQueryable().Where(p => p.UserId == userId).MaxAsync(p => (int?)p.Version) ?? 0;
await _planRepository.UpdateAsync(p => new HealthPlan { IsCurrent = false }, p => p.UserId == userId && p.IsCurrent);
var planItems = BuildPlanItems(profile, modules);
var plan = new HealthPlan
{
UserId = userId,
Version = maxVersion + 1,
Title = $"个性化健康方案 v{maxVersion + 1}.0",
Summary = BuildAssessmentPlanSummary(profile, input.Answers),
ParametersJson = JsonSerializer.Serialize(planItems),
IsCurrent = true
};
await _planRepository.InsertAsync(plan);
return await GetAssessment();
}
[Authorize]
[HttpPut("/api/health/profile")]
public async Task<SaveHealthProfileInput> SaveProfile(SaveHealthProfileInput input)
@@ -328,4 +464,200 @@ public sealed class HealthAppService : IDynamicApiController, ITransient
User = MapUser(user)
};
}
private static void ValidateAssessment(SaveHealthAssessmentInput input)
{
var info = input.BasicInfo ?? throw Oops.Oh("请填写个人基本信息");
var answers = input.Answers ?? throw Oops.Oh("请完成健康评估问卷");
if (info.Gender is not ("男" or "女")) throw Oops.Oh("请选择性别");
if (info.Birthday is null) throw Oops.Oh("请填写年龄");
var age = DateTime.Today.Year - info.Birthday.Value.Year;
if (info.Birthday.Value.Date > DateTime.Today.AddYears(-age)) age--;
if (age is < 1 or > 120) throw Oops.Oh("年龄应在 1 到 120 岁之间");
if (info.HeightCm is null or < 80 or > 250) throw Oops.Oh("身高应在 80 到 250cm 之间");
if (info.WeightKg is null or < 20 or > 300) throw Oops.Oh("体重应在 20 到 300kg 之间");
RequireChoice(answers.SleepDuration, "每晚平均睡眠时长", "<6", "6-7", "7-8", ">8");
RequireChoice(answers.FallingAsleepSpeed, "入睡耗时", "hard", "normal", "fast");
RequireChoice(answers.TemperaturePreference, "温度偏好", "heat_fear", "cold_fear", "normal");
RequireChoice(answers.NightAwakening, "夜醒情况", "frequent", "light", "none");
RequireChoice(answers.MattressPreference, "床垫偏好", "firm", "medium", "soft");
RequireChoice(answers.ExerciseFrequency, "运动频率", "none", "1-2", "3-5", "daily");
RequireChoice(answers.MuscleFatigueArea, "疲劳部位", "back_neck", "waist_back", "legs", "none");
RequireChoice(answers.ActiveTimeOfDay, "活跃时段", "morning", "evening", "night", "irregular");
RequireChoice(answers.MealRegularity, "三餐规律", "regular", "skip_breakfast", "irregular");
RequireChoice(answers.FlavorPreference, "口味偏好", "light", "moderate", "heavy");
RequireChoice(answers.LateNightEating, "夜宵频率", "often", "sometimes", "rarely");
RequireChoice(answers.DailyWaterIntake, "饮水情况", "abundant", "moderate", "scarce");
}
private static void RequireChoice(string value, string label, params string[] allowed)
{
if (string.IsNullOrWhiteSpace(value) || !allowed.Contains(value, StringComparer.Ordinal))
throw Oops.Oh($"请完成“{label}”问题");
}
private static PersonalizedSceneConfig CalculatePersonalizedSceneConfig(HealthAssessmentAnswers answers, SaveHealthProfileInput basicInfo)
{
var config = new PersonalizedSceneConfig();
switch (answers.TemperaturePreference)
{
case "heat_fear":
config.TargetHumidity = 48;
config.FanLevel = "2挡";
config.MattressTargetTemp = 32m;
config.MattressHeatingLevel = 1;
break;
case "cold_fear":
config.TargetHumidity = 58;
config.FanLevel = "1挡";
config.MattressTargetTemp = 37.5m;
config.MattressHeatingLevel = 3;
break;
default:
config.TargetHumidity = 52;
config.FanLevel = "1挡";
config.MattressTargetTemp = 35.5m;
config.MattressHeatingLevel = 2;
break;
}
switch (answers.FallingAsleepSpeed)
{
case "hard":
config.LightBrightnessPercent = 8;
config.LightColorTemperatureKelvin = 2200;
config.FragranceLevel = 2;
config.EnableSpeakerSleepMode = true;
config.SpeakerAudioMode = "助眠白噪音";
config.SpeakerVolumePercent = 12;
break;
case "fast":
config.LightBrightnessPercent = 25;
config.LightColorTemperatureKelvin = 3000;
config.EnableSpeakerSleepMode = false;
config.SpeakerAudioMode = "关闭助眠音频";
config.SpeakerVolumePercent = 10;
break;
default:
config.LightBrightnessPercent = 18;
config.LightColorTemperatureKelvin = 2700;
config.FragranceLevel = 1;
config.EnableSpeakerSleepMode = true;
config.SpeakerAudioMode = "舒缓轻音乐";
config.SpeakerVolumePercent = 15;
break;
}
switch (answers.SleepDuration)
{
case "<6":
config.LightBrightnessPercent -= 3;
config.SpeakerVolumePercent += 3;
config.FragranceLevel++;
break;
case "6-7":
config.LightBrightnessPercent -= 1;
config.SpeakerVolumePercent += 1;
break;
case ">8":
config.LightBrightnessPercent += 2;
config.SpeakerVolumePercent -= 2;
break;
}
if (answers.MuscleFatigueArea is "waist_back" or "back_neck" || answers.MattressPreference == "firm")
{
config.MattressLeftFirmness = 7;
config.MattressRightFirmness = 7;
config.MattressMode = "智能腰托";
}
else if (answers.MattressPreference == "soft")
{
config.MattressLeftFirmness = 4;
config.MattressRightFirmness = 4;
config.MattressMode = "瑜伽助眠";
}
else
{
config.MattressLeftFirmness = answers.MuscleFatigueArea == "legs" ? 6 : 5;
config.MattressRightFirmness = answers.MuscleFatigueArea == "legs" ? 6 : 5;
config.MattressMode = "智能适配";
}
config.DehumidifierMode = answers.NightAwakening == "none" ? "智能" : "睡眠";
if (answers.NightAwakening == "frequent")
{
config.FanLevel = "1挡";
config.LightBrightnessPercent -= 2;
}
switch (answers.ExerciseFrequency)
{
case "none": config.MattressHeatingLevel++; break;
case "daily": config.MattressHeatingLevel--; break;
}
switch (answers.ActiveTimeOfDay)
{
case "morning": config.LightColorTemperatureKelvin += 150; break;
case "night":
config.LightColorTemperatureKelvin -= 250;
config.LightBrightnessPercent -= 2;
break;
case "irregular":
config.EnableSpeakerSleepMode = true;
config.SpeakerVolumePercent += 2;
break;
}
config.EnableUv = answers.LateNightEating is "often" or "sometimes" || answers.FlavorPreference == "heavy";
if (answers.MealRegularity == "irregular")
{
config.EnableUv = true;
config.FragranceLevel++;
}
else if (answers.MealRegularity == "skip_breakfast")
{
config.FragranceLevel++;
}
if (answers.DailyWaterIntake == "scarce") config.TargetHumidity += 3;
if (answers.DailyWaterIntake == "abundant") config.TargetHumidity -= 2;
if (basicInfo.HeightCm > 0 && basicInfo.WeightKg > 0)
{
var height = basicInfo.HeightCm.Value / 100m;
var bmi = basicInfo.WeightKg.Value / (height * height);
if (bmi < 18.5m) config.MattressHeatingLevel++;
if (bmi >= 28m) config.TargetHumidity -= 2;
}
if (basicInfo.Birthday.HasValue)
{
var age = DateTime.Today.Year - basicInfo.Birthday.Value.Year;
if (age >= 60)
{
config.LightBrightnessPercent += 3;
config.SpeakerVolumePercent = Math.Max(8, config.SpeakerVolumePercent - 2);
}
}
config.TargetHumidity = Math.Clamp(config.TargetHumidity, 40, 70);
config.LightBrightnessPercent = Math.Clamp(config.LightBrightnessPercent, 1, 100);
config.LightColorTemperatureKelvin = Math.Clamp(config.LightColorTemperatureKelvin, 2200, 5000);
config.SpeakerVolumePercent = Math.Clamp(config.SpeakerVolumePercent, 1, 50);
config.FragranceLevel = Math.Clamp(config.FragranceLevel, 1, 3);
config.MattressHeatingLevel = Math.Clamp(config.MattressHeatingLevel, 0, 4);
return config;
}
private static string BuildAssessmentPlanSummary(HealthProfile profile, HealthAssessmentAnswers answers)
{
var sleepNote = answers.FallingAsleepSpeed == "hard" ? "针对入睡耗时较长启用了音箱睡眠模式与极暗暖光;" : "";
var tempNote = answers.TemperaturePreference == "heat_fear" ? "依据体感偏热降低了床垫加热档位与室内湿度;" : answers.TemperaturePreference == "cold_fear" ? "依据体感偏冷提高了床垫保温档位;" : "";
var supportNote = answers.MuscleFatigueArea is "waist_back" or "back_neck" ? "针对腰背与颈部疲劳启用了床垫智能腰托模式;" : "";
return $"已完成全面健康评估,{sleepNote}{tempNote}{supportNote}全套智能家具启动参数已完成个性化配置。";
}
}
@@ -18,6 +18,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
private readonly HomeAssistantRegistryService _registryService;
private readonly HomeAssistantOptions _options;
private readonly SqlSugarRepository<HealthSceneExecution> _executionRepository;
private readonly SqlSugarRepository<HealthProfile> _profileRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
public HealthHomeAssistantService(
@@ -25,12 +26,14 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
HomeAssistantRegistryService registryService,
IOptions<HomeAssistantOptions> options,
SqlSugarRepository<HealthSceneExecution> executionRepository,
SqlSugarRepository<HealthProfile> profileRepository,
IHttpContextAccessor httpContextAccessor)
{
_homeAssistant = homeAssistant;
_registryService = registryService;
_options = options.Value;
_executionRepository = executionRepository;
_profileRepository = profileRepository;
_httpContextAccessor = httpContextAccessor;
}
@@ -48,7 +51,22 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
var summary = BuildSummary(discoveredDevices);
var sceneDevices = BuildSceneDevicesSummary(stateMap);
var environment = BuildEnvironmentOutput(stateMap, discoveredDevices);
var scenePresets = BuildScenePresets(stateMap);
PersonalizedSceneConfig? userConfig = null;
try
{
var userId = GetCurrentUserId();
var profile = await _profileRepository.GetFirstAsync(p => p.UserId == userId);
if (!string.IsNullOrWhiteSpace(profile?.CustomSceneConfigJson))
{
userConfig = JsonSerializer.Deserialize<PersonalizedSceneConfig>(profile.CustomSceneConfigJson);
}
}
catch
{
}
var scenePresets = BuildScenePresets(stateMap, userConfig);
return new HomeAssistantStatusOutput
{
@@ -59,7 +77,8 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
Devices = discoveredDevices,
SceneDevices = sceneDevices,
Environment = environment,
ScenePresets = scenePresets
ScenePresets = scenePresets,
ControlParameters = BuildControlParameterStates(stateMap)
};
}
catch (Exception exception)
@@ -73,7 +92,8 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
Devices = [],
SceneDevices = BuildSceneDevicesSummary(new Dictionary<string, HomeAssistantState>(StringComparer.OrdinalIgnoreCase)),
Environment = new HomeAssistantEnvironmentOutput(),
ScenePresets = BuildScenePresets(new Dictionary<string, HomeAssistantState>(StringComparer.OrdinalIgnoreCase))
ScenePresets = BuildScenePresets(new Dictionary<string, HomeAssistantState>(StringComparer.OrdinalIgnoreCase)),
ControlParameters = []
};
}
}
@@ -190,10 +210,220 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
}
}
[Authorize]
[HttpPost("/api/health/home-assistant/devices/set-parameter")]
public async Task<HomeAssistantSetParameterOutput> SetParameter(
[FromBody] HomeAssistantSetParameterInput input,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(input.DeviceId) || string.IsNullOrWhiteSpace(input.ParameterType))
{
return new HomeAssistantSetParameterOutput
{
Success = false,
DeviceId = input.DeviceId,
ParameterType = input.ParameterType,
Message = "未指定控制设备或参数"
};
}
try
{
var states = await _homeAssistant.GetStatesAsync(cancellationToken);
var stateMap = states.ToDictionary(item => item.EntityId, StringComparer.OrdinalIgnoreCase);
var registries = await _registryService.GetRegistriesAsync(cancellationToken);
var discovered = BuildDiscoveredDevices(registries, stateMap);
var targetDevice = discovered.FirstOrDefault(d =>
string.Equals(d.DeviceId, input.DeviceId, StringComparison.OrdinalIgnoreCase));
if (targetDevice is null)
{
return new HomeAssistantSetParameterOutput
{
Success = false,
DeviceId = input.DeviceId,
ParameterType = input.ParameterType,
Message = "未找到指定设备"
};
}
var parameterType = input.ParameterType.Trim().ToLowerInvariant();
string targetEntityId;
string domain;
string service;
var serviceData = new Dictionary<string, object?>();
switch (parameterType)
{
case "humidity":
targetEntityId = _options.Entities.Dehumidifier;
domain = "humidifier";
service = "set_humidity";
serviceData["humidity"] = ReadInt(input.Value, 40, 70, "目标湿度");
break;
case "fan_level":
targetEntityId = _options.Entities.DehumidifierFanLevel;
domain = "select";
service = "select_option";
var fanLevel = ReadString(input.Value).Replace('档', '挡');
if (fanLevel is not ("1挡" or "2挡" or "3挡")) throw new InvalidOperationException("风速档位无效");
serviceData["option"] = fanLevel;
break;
case "dehumidifier_mode":
targetEntityId = _options.Entities.Dehumidifier;
domain = "humidifier";
service = "set_mode";
var dehumidifierMode = ReadString(input.Value);
if (dehumidifierMode is not ("睡眠" or "智能")) throw new InvalidOperationException("除湿模式无效");
serviceData["mode"] = dehumidifierMode;
break;
case "brightness":
targetEntityId = _options.Entities.AmbientLight;
domain = "light";
service = "turn_on";
serviceData["brightness_pct"] = ReadInt(input.Value, 1, 100, "灯光亮度");
break;
case "color_temp":
targetEntityId = _options.Entities.AmbientLight;
domain = "light";
service = "turn_on";
serviceData["color_temp_kelvin"] = ReadInt(input.Value, 2200, 5000, "灯光色温");
break;
case "volume":
targetEntityId = _options.Entities.Speaker;
domain = "media_player";
service = "volume_set";
serviceData["volume_level"] = ReadInt(input.Value, 0, 100, "音箱音量") / 100d;
break;
case "speaker_sleep_mode":
targetEntityId = _options.Entities.SpeakerSleepMode;
domain = "switch";
service = ReadBool(input.Value) ? "turn_on" : "turn_off";
break;
case "mattress_left_heating":
targetEntityId = _options.Entities.MattressLeftHeatingLevel;
domain = "number";
service = "set_value";
serviceData["value"] = ReadInt(input.Value, 0, 4, "床垫左侧加热档位");
break;
case "mattress_right_heating":
targetEntityId = _options.Entities.MattressRightHeatingLevel;
domain = "number";
service = "set_value";
serviceData["value"] = ReadInt(input.Value, 0, 4, "床垫右侧加热档位");
break;
case "mattress_left_mode":
targetEntityId = _options.Entities.MattressLeftMode;
domain = "select";
service = "select_option";
serviceData["option"] = ReadMattressMode(input.Value);
break;
case "mattress_right_mode":
targetEntityId = _options.Entities.MattressRightMode;
domain = "select";
service = "select_option";
serviceData["option"] = ReadMattressMode(input.Value);
break;
case "uv":
targetEntityId = _options.Entities.DehumidifierUv;
domain = "switch";
service = ReadBool(input.Value) ? "turn_on" : "turn_off";
break;
default:
return new HomeAssistantSetParameterOutput
{
Success = false,
DeviceId = input.DeviceId,
ParameterType = input.ParameterType,
Message = $"不支持的参数类型: {input.ParameterType}"
};
}
if (string.IsNullOrWhiteSpace(targetEntityId))
throw new InvalidOperationException("该参数尚未配置 Home Assistant 实体");
if (!string.IsNullOrWhiteSpace(input.EntityId) &&
!string.Equals(input.EntityId, targetEntityId, StringComparison.OrdinalIgnoreCase))
throw new InvalidOperationException("请求实体与该参数的受信任实体不匹配");
if (!targetDevice.Entities.Any(e => string.Equals(e.EntityId, targetEntityId, StringComparison.OrdinalIgnoreCase)))
throw new InvalidOperationException("该参数不属于指定设备");
if (!stateMap.TryGetValue(targetEntityId, out var targetState) || !IsAvailable(targetState))
throw new InvalidOperationException("目标参数实体当前离线或不可用");
serviceData["entity_id"] = targetEntityId;
await _homeAssistant.CallServiceAsync(domain, service, serviceData, cancellationToken);
try
{
var userId = GetCurrentUserId();
var profile = await _profileRepository.GetFirstAsync(p => p.UserId == userId);
if (profile != null)
{
var customConfig = string.IsNullOrWhiteSpace(profile.CustomSceneConfigJson)
? new PersonalizedSceneConfig()
: JsonSerializer.Deserialize<PersonalizedSceneConfig>(profile.CustomSceneConfigJson) ?? new PersonalizedSceneConfig();
switch (parameterType)
{
case "humidity": customConfig.TargetHumidity = (int)serviceData["humidity"]!; break;
case "fan_level": customConfig.FanLevel = (string)serviceData["option"]!; break;
case "dehumidifier_mode": customConfig.DehumidifierMode = (string)serviceData["mode"]!; break;
case "brightness": customConfig.LightBrightnessPercent = (int)serviceData["brightness_pct"]!; break;
case "color_temp": customConfig.LightColorTemperatureKelvin = (int)serviceData["color_temp_kelvin"]!; break;
case "volume": customConfig.SpeakerVolumePercent = ReadInt(input.Value, 0, 100, "音箱音量"); break;
case "speaker_sleep_mode": customConfig.EnableSpeakerSleepMode = ReadBool(input.Value); break;
case "uv": customConfig.EnableUv = ReadBool(input.Value); break;
case "mattress_left_heating":
case "mattress_right_heating": customConfig.MattressHeatingLevel = ReadInt(input.Value, 0, 4, "床垫加热档位"); break;
case "mattress_left_mode":
case "mattress_right_mode": customConfig.MattressMode = ReadMattressMode(input.Value); break;
}
profile.CustomSceneConfigJson = JsonSerializer.Serialize(customConfig);
await _profileRepository.UpdateAsync(profile);
}
}
catch
{
}
return new HomeAssistantSetParameterOutput
{
Success = true,
DeviceId = input.DeviceId,
ParameterType = input.ParameterType ?? "",
Message = "参数调节成功"
};
}
catch (Exception ex)
{
return new HomeAssistantSetParameterOutput
{
Success = false,
DeviceId = input.DeviceId,
ParameterType = input.ParameterType ?? "",
Message = $"调节失败:{ex.Message}"
};
}
}
[Authorize]
[HttpPost("/api/health/home-assistant/scenes/sleep")]
public Task<HomeAssistantSceneOutput> ExecuteSleepScene(CancellationToken cancellationToken)
=> ExecuteScene("sleep", "舒睡模式", ExecuteSleepActions, cancellationToken);
public async Task<HomeAssistantSceneOutput> ExecuteSleepScene(CancellationToken cancellationToken)
{
PersonalizedSceneConfig? customConfig = null;
try
{
var userId = GetCurrentUserId();
var profile = await _profileRepository.GetFirstAsync(p => p.UserId == userId);
if (!string.IsNullOrWhiteSpace(profile?.CustomSceneConfigJson))
{
customConfig = JsonSerializer.Deserialize<PersonalizedSceneConfig>(profile.CustomSceneConfigJson);
}
}
catch
{
}
return await ExecuteScene("sleep", "舒睡模式", (states, results, ct) => ExecuteSleepActions(states, results, customConfig, ct), cancellationToken);
}
[Authorize]
[HttpPost("/api/health/home-assistant/scenes/all-off")]
@@ -264,48 +494,69 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
private async Task ExecuteSleepActions(
IReadOnlyDictionary<string, HomeAssistantState> states,
List<HomeAssistantActionResult> results,
PersonalizedSceneConfig? customConfig,
CancellationToken cancellationToken)
{
var entities = _options.Entities;
var scene = _options.SleepScene;
var targetHumidity = customConfig?.TargetHumidity ?? scene.TargetHumidity;
var dehumMode = customConfig?.DehumidifierMode ?? scene.DehumidifierMode;
var fanLevel = customConfig?.FanLevel ?? scene.FanLevel;
var enableUv = customConfig?.EnableUv ?? scene.EnableUv;
var lightBrightness = customConfig?.LightBrightnessPercent ?? scene.LightBrightnessPercent;
var lightColorTemp = customConfig?.LightColorTemperatureKelvin ?? scene.LightColorTemperatureKelvin;
var enableFragrance = customConfig?.EnableFragrance ?? scene.EnableFragrance;
var enableSpeakerSleepMode = customConfig?.EnableSpeakerSleepMode ?? scene.EnableSpeakerSleepMode;
var speakerVolume = customConfig?.SpeakerVolumePercent ?? 15;
var mattressHeatingLevel = customConfig?.MattressHeatingLevel ?? scene.MattressHeatingLevel;
var mattressMode = customConfig?.MattressMode ?? scene.MattressMode;
await RunAction(states, results, "开启除湿机", entities.Dehumidifier, "humidifier", "turn_on", null, cancellationToken);
await RunAction(states, results, "设置目标湿度", entities.Dehumidifier, "humidifier", "set_humidity",
new Dictionary<string, object?> { ["humidity"] = scene.TargetHumidity }, cancellationToken);
new Dictionary<string, object?> { ["humidity"] = targetHumidity }, cancellationToken);
await RunAction(states, results, "切换睡眠模式", entities.Dehumidifier, "humidifier", "set_mode",
new Dictionary<string, object?> { ["mode"] = scene.DehumidifierMode }, cancellationToken);
new Dictionary<string, object?> { ["mode"] = dehumMode }, cancellationToken);
await RunAction(states, results, "设置低档风速", entities.DehumidifierFanLevel, "select", "select_option",
new Dictionary<string, object?> { ["option"] = scene.FanLevel }, cancellationToken);
await RunAction(states, results, scene.EnableUv ? "开启 UV 杀菌" : "关闭 UV 杀菌", entities.DehumidifierUv,
"switch", scene.EnableUv ? "turn_on" : "turn_off", null, cancellationToken);
new Dictionary<string, object?> { ["option"] = fanLevel }, cancellationToken);
await RunAction(states, results, enableUv ? "开启 UV 杀菌" : "关闭 UV 杀菌", entities.DehumidifierUv,
"switch", enableUv ? "turn_on" : "turn_off", null, cancellationToken);
if (scene.EnableFragrance)
if (enableFragrance)
{
await RunAction(states, results, "开启香氛机", entities.FragranceSwitch, "switch", "turn_on", null, cancellationToken);
await RunAction(states, results, "开启香氛灯", entities.FragranceLight, "light", "turn_on",
new Dictionary<string, object?> { ["brightness_pct"] = scene.LightBrightnessPercent }, cancellationToken);
new Dictionary<string, object?> { ["brightness_pct"] = lightBrightness }, cancellationToken);
}
await RunAction(states, results, "开启助眠灯光", entities.AmbientLight, "light", "turn_on",
new Dictionary<string, object?>
{
["brightness_pct"] = scene.LightBrightnessPercent,
["color_temp_kelvin"] = scene.LightColorTemperatureKelvin
["brightness_pct"] = lightBrightness,
["color_temp_kelvin"] = lightColorTemp
}, cancellationToken);
if (scene.EnableSpeakerSleepMode)
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<string, object?> { ["volume_level"] = speakerVolume / 100d }, cancellationToken);
}
else
{
await RunAction(states, results, "关闭音箱睡眠模式", entities.SpeakerSleepMode, "switch", "turn_off", null, cancellationToken);
}
if (scene.EnableMattress)
{
await RunAction(states, results, "设置床垫左侧低档加热", entities.MattressLeftHeatingLevel, "number", "set_value",
new Dictionary<string, object?> { ["value"] = scene.MattressHeatingLevel }, cancellationToken);
await RunAction(states, results, "设置床垫右侧低档加热", entities.MattressRightHeatingLevel, "number", "set_value",
new Dictionary<string, object?> { ["value"] = scene.MattressHeatingLevel }, cancellationToken);
await RunAction(states, results, "设置床垫左侧智能适配", entities.MattressLeftMode, "select", "select_option",
new Dictionary<string, object?> { ["option"] = scene.MattressMode }, cancellationToken);
await RunAction(states, results, "设置床垫右侧智能适配", entities.MattressRightMode, "select", "select_option",
new Dictionary<string, object?> { ["option"] = scene.MattressMode }, cancellationToken);
await RunAction(states, results, "设置床垫左侧加热档位", entities.MattressLeftHeatingLevel, "number", "set_value",
new Dictionary<string, object?> { ["value"] = mattressHeatingLevel }, cancellationToken);
await RunAction(states, results, "设置床垫右侧加热档位", entities.MattressRightHeatingLevel, "number", "set_value",
new Dictionary<string, object?> { ["value"] = mattressHeatingLevel }, cancellationToken);
await RunAction(states, results, "设置床垫左侧模式", entities.MattressLeftMode, "select", "select_option",
new Dictionary<string, object?> { ["option"] = mattressMode }, cancellationToken);
await RunAction(states, results, "设置床垫右侧模式", entities.MattressRightMode, "select", "select_option",
new Dictionary<string, object?> { ["option"] = mattressMode }, cancellationToken);
}
}
@@ -733,10 +984,85 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
return safe.Count > 0 ? safe : null;
}
private List<HomeAssistantScenePresetItem> BuildScenePresets(
private Dictionary<string, HomeAssistantParameterStateOutput> BuildControlParameterStates(
IReadOnlyDictionary<string, HomeAssistantState> stateMap)
{
var scene = _options.SleepScene;
var configured = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["humidity"] = _options.Entities.Dehumidifier,
["fan_level"] = _options.Entities.DehumidifierFanLevel,
["uv"] = _options.Entities.DehumidifierUv,
["brightness"] = _options.Entities.AmbientLight,
["color_temp"] = _options.Entities.AmbientLight,
["volume"] = _options.Entities.Speaker,
["speaker_sleep_mode"] = _options.Entities.SpeakerSleepMode,
["mattress_left_heating"] = _options.Entities.MattressLeftHeatingLevel,
["mattress_right_heating"] = _options.Entities.MattressRightHeatingLevel,
["mattress_left_mode"] = _options.Entities.MattressLeftMode,
["mattress_right_mode"] = _options.Entities.MattressRightMode
};
var result = new Dictionary<string, HomeAssistantParameterStateOutput>(StringComparer.OrdinalIgnoreCase);
foreach (var (parameterType, entityId) in configured)
{
if (string.IsNullOrWhiteSpace(entityId) || !stateMap.TryGetValue(entityId, out var state)) continue;
result[parameterType] = new HomeAssistantParameterStateOutput
{
EntityId = entityId,
State = state.State,
Available = IsAvailable(state),
Attributes = ExtractSafeAttributes(state)
};
}
return result;
}
private static string ReadString(object? value)
{
if (value is JsonElement element)
{
return element.ValueKind == JsonValueKind.String
? element.GetString()?.Trim() ?? string.Empty
: element.ToString().Trim();
}
return value?.ToString()?.Trim() ?? string.Empty;
}
private static int ReadInt(object? value, int min, int max, string label)
{
if (!int.TryParse(ReadString(value), out var parsed) || parsed < min || parsed > max)
throw new InvalidOperationException($"{label}应在 {min} 到 {max} 之间");
return parsed;
}
private static bool ReadBool(object? value)
{
if (value is JsonElement element && element.ValueKind is JsonValueKind.True or JsonValueKind.False)
return element.GetBoolean();
if (bool.TryParse(ReadString(value), out var parsed)) return parsed;
throw new InvalidOperationException("开关参数无效");
}
private static string ReadMattressMode(object? value)
{
var mode = ReadString(value);
if (mode is not ("无" or "瑜伽助眠" or "智能适配" or "智能腰托"))
throw new InvalidOperationException("床垫模式无效");
return mode;
}
private List<HomeAssistantScenePresetItem> BuildScenePresets(
IReadOnlyDictionary<string, HomeAssistantState> stateMap,
PersonalizedSceneConfig? userConfig = null)
{
var defaultScene = _options.SleepScene;
var targetHumidity = userConfig?.TargetHumidity ?? defaultScene.TargetHumidity;
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;
var presets = new List<HomeAssistantScenePresetItem>();
if (!string.IsNullOrWhiteSpace(_options.Entities.Dehumidifier))
@@ -747,18 +1073,18 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
Category = nameof(HealthDeviceCategory.EnvironmentControl),
EntityId = _options.Entities.Dehumidifier,
Action = "开启恒湿舒睡",
Detail = $"目标湿度 {scene.TargetHumidity}% · {scene.DehumidifierMode}模式 · 风速{scene.FanLevel} · UV{(scene.EnableUv ? "开" : "关")}",
Detail = $"目标湿度 {targetHumidity}% · {dehumMode}模式 · 风速{fanLevel} · UV{(enableUv ? "开" : "关")}",
Parameters = new()
{
["target_humidity"] = $"{scene.TargetHumidity}%",
["mode"] = scene.DehumidifierMode,
["fan_level"] = scene.FanLevel,
["uv"] = scene.EnableUv ? "开启" : "关闭"
["target_humidity"] = $"{targetHumidity}%",
["mode"] = dehumMode,
["fan_level"] = fanLevel,
["uv"] = enableUv ? "开启" : "关闭"
}
});
}
if (scene.EnableFragrance && !string.IsNullOrWhiteSpace(_options.Entities.FragranceSwitch))
if (enableFragrance && !string.IsNullOrWhiteSpace(_options.Entities.FragranceSwitch))
{
presets.Add(new HomeAssistantScenePresetItem
{
@@ -779,43 +1105,44 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
Category = nameof(HealthDeviceCategory.Lighting),
EntityId = _options.Entities.AmbientLight,
Action = "开启暖光助眠",
Detail = $"亮度 {scene.LightBrightnessPercent}% · 色温 {scene.LightColorTemperatureKelvin}K",
Detail = $"亮度 {lightBrightness}% · 色温 {lightColorTemp}K",
Parameters = new()
{
["brightness"] = $"{scene.LightBrightnessPercent}%",
["color_temp"] = $"{scene.LightColorTemperatureKelvin}K"
["brightness"] = $"{lightBrightness}%",
["color_temp"] = $"{lightColorTemp}K"
}
});
}
if (scene.EnableSpeakerSleepMode && !string.IsNullOrWhiteSpace(_options.Entities.SpeakerSleepMode))
if (enableSpeaker && !string.IsNullOrWhiteSpace(_options.Entities.SpeakerSleepMode))
{
var vol = userConfig?.SpeakerVolumePercent ?? 15;
presets.Add(new HomeAssistantScenePresetItem
{
DeviceName = "Xiaomi Sound Pro",
Category = nameof(HealthDeviceCategory.Media),
EntityId = _options.Entities.SpeakerSleepMode,
Action = "开启音箱睡眠模式",
Detail = "仅切换已确认的睡眠模式开关,不自动播放未知媒体内容",
Parameters = new() { ["sleep_mode"] = "开启" }
Detail = $"切换睡眠模式开关 · 音量 {vol}%",
Parameters = new() { ["sleep_mode"] = "开启", ["volume"] = $"{vol}%" }
});
}
if (scene.EnableMattress && !string.IsNullOrWhiteSpace(_options.Entities.MattressLeftHeatingLevel))
if (defaultScene.EnableMattress && !string.IsNullOrWhiteSpace(_options.Entities.MattressLeftHeatingLevel))
{
var heatingLevel = userConfig?.MattressHeatingLevel ?? defaultScene.MattressHeatingLevel;
var mattressMode = userConfig?.MattressMode ?? defaultScene.MattressMode;
presets.Add(new HomeAssistantScenePresetItem
{
DeviceName = "8H AI智能好睡眠床垫X",
Category = nameof(HealthDeviceCategory.Sleep),
EntityId = _options.Entities.MattressLeftHeatingLevel,
Action = "开启低档加热与智能适配",
Detail = $"左右加热 {scene.MattressHeatingLevel} 档 · 左右气袋模式 {scene.MattressMode}",
Action = "开启分区加热与承托模式",
Detail = $"左右加热 {heatingLevel} 档 · {mattressMode}",
Parameters = new()
{
["left_heating_level"] = scene.MattressHeatingLevel,
["right_heating_level"] = scene.MattressHeatingLevel,
["left_mode"] = scene.MattressMode,
["right_mode"] = scene.MattressMode
["heating_level"] = heatingLevel,
["mode"] = mattressMode
}
});
}