using Evergreen.Health.Application.Health.Entities; using Evergreen.Health.Application.Health.Integrations.HomeAssistant; using Evergreen.Health.Application.Health.Models; using Furion.DatabaseAccessor; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using SqlSugar; using System.Security.Claims; using System.Text.Json; namespace Evergreen.Health.Application.Health.Services; [ApiDescriptionSettings("健康管理", Name = "HealthHomeAssistant", Order = 51)] public sealed class HealthHomeAssistantService : IDynamicApiController, ITransient { private const string HealthUserIdClaim = "health_user_id"; private readonly HomeAssistantClient _homeAssistant; private readonly HomeAssistantOptions _options; private readonly SqlSugarRepository _executionRepository; private readonly IHttpContextAccessor _httpContextAccessor; public HealthHomeAssistantService( HomeAssistantClient homeAssistant, IOptions options, SqlSugarRepository executionRepository, IHttpContextAccessor httpContextAccessor) { _homeAssistant = homeAssistant; _options = options.Value; _executionRepository = executionRepository; _httpContextAccessor = httpContextAccessor; } [Authorize] [HttpGet("/api/health/home-assistant/status")] public async Task GetStatus(CancellationToken cancellationToken) { try { var states = await _homeAssistant.GetStatesAsync(cancellationToken); var stateMap = states.ToDictionary(item => item.EntityId, StringComparer.OrdinalIgnoreCase); var entities = _options.Entities; return new HomeAssistantStatusOutput { Connected = true, Message = "Home Assistant 已连接", Devices = [ BuildDevice(stateMap, "dehumidifier", "除湿机", entities.Dehumidifier, "current_humidity", "%"), BuildDevice(stateMap, "fragrance", "香氛机", entities.FragranceSwitch), BuildDevice(stateMap, "ambient-light", "氛围灯", entities.AmbientLight), BuildDevice(stateMap, "speaker", "Xiaomi Sound Pro", entities.Speaker), ], Environment = [ BuildDevice(stateMap, "temperature", "温度", entities.Temperature), BuildDevice(stateMap, "humidity", "湿度", entities.Humidity), BuildDevice(stateMap, "pm25", "PM2.5", entities.Pm25), BuildDevice(stateMap, "co2", "CO₂", entities.Co2), BuildDevice(stateMap, "formaldehyde", "甲醛", entities.Formaldehyde), BuildDevice(stateMap, "tvoc", "TVOC", entities.Tvoc), ] }; } catch (Exception exception) { return new HomeAssistantStatusOutput { Connected = false, Message = $"Home Assistant 连接失败:{exception.Message}" }; } } [Authorize] [HttpPost("/api/health/home-assistant/scenes/sleep")] public Task ExecuteSleepScene(CancellationToken cancellationToken) => ExecuteScene("sleep", "舒睡模式", ExecuteSleepActions, cancellationToken); [Authorize] [HttpPost("/api/health/home-assistant/scenes/all-off")] public Task ExecuteAllOffScene(CancellationToken cancellationToken) => ExecuteScene("all-off", "一键关闭", ExecuteAllOffActions, cancellationToken); private async Task ExecuteScene( string sceneCode, string sceneName, Func, List, CancellationToken, Task> executor, CancellationToken cancellationToken) { var userId = GetCurrentUserId(); var actions = new List(); string status; string message; try { var states = await _homeAssistant.GetStatesAsync(cancellationToken); var stateMap = states.ToDictionary(item => item.EntityId, StringComparer.OrdinalIgnoreCase); await executor(stateMap, actions, cancellationToken); var successCount = actions.Count(item => item.Status == "Succeeded"); var failedCount = actions.Count(item => item.Status == "Failed"); var skippedCount = actions.Count(item => item.Status == "Skipped"); 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} 项跳过", _ => "当前没有可执行的在线设备" }; } catch (Exception exception) { status = "Failed"; message = $"场景执行失败:{exception.Message}"; } await _executionRepository.InsertAsync(new HealthSceneExecution { UserId = userId, SceneCode = sceneCode, SceneName = sceneName, Status = status, Detail = JsonSerializer.Serialize(new { message, actions }) }); return new HomeAssistantSceneOutput { SceneCode = sceneCode, SceneName = sceneName, Success = status is "Succeeded" or "Partial", Status = status, Message = message, Actions = actions.Select(item => new HomeAssistantSceneActionOutput { Name = item.Name, EntityId = item.EntityId, Status = item.Status, Message = item.Message }).ToList() }; } private async Task ExecuteSleepActions( IReadOnlyDictionary states, List results, CancellationToken cancellationToken) { var entities = _options.Entities; var scene = _options.SleepScene; await RunAction(states, results, "开启除湿机", entities.Dehumidifier, "humidifier", "turn_on", null, cancellationToken); await RunAction(states, results, "设置目标湿度", entities.Dehumidifier, "humidifier", "set_humidity", new Dictionary { ["humidity"] = scene.TargetHumidity }, cancellationToken); await RunAction(states, results, "切换睡眠模式", entities.Dehumidifier, "humidifier", "set_mode", new Dictionary { ["mode"] = scene.DehumidifierMode }, cancellationToken); await RunAction(states, results, "设置低档风速", entities.DehumidifierFanLevel, "select", "select_option", new Dictionary { ["option"] = scene.FanLevel }, cancellationToken); await RunAction(states, results, scene.EnableUv ? "开启 UV 杀菌" : "关闭 UV 杀菌", entities.DehumidifierUv, "switch", scene.EnableUv ? "turn_on" : "turn_off", null, cancellationToken); if (scene.EnableFragrance) { await RunAction(states, results, "开启香氛机", entities.FragranceSwitch, "switch", "turn_on", null, cancellationToken); await RunAction(states, results, "开启香氛灯", entities.FragranceLight, "light", "turn_on", new Dictionary { ["brightness_pct"] = scene.LightBrightnessPercent }, cancellationToken); } await RunAction(states, results, "开启助眠灯光", entities.AmbientLight, "light", "turn_on", new Dictionary { ["brightness_pct"] = scene.LightBrightnessPercent, ["color_temp_kelvin"] = scene.LightColorTemperatureKelvin }, cancellationToken); if (scene.EnableSpeakerSleepMode) await RunAction(states, results, "开启音箱睡眠模式", entities.SpeakerSleepMode, "switch", "turn_on", null, cancellationToken); } private async Task ExecuteAllOffActions( IReadOnlyDictionary states, List results, CancellationToken cancellationToken) { var entities = _options.Entities; 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); } private async Task RunAction( IReadOnlyDictionary states, List results, string name, string entityId, string domain, string service, IReadOnlyDictionary? extraData, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(entityId) || !states.TryGetValue(entityId, out var state)) { results.Add(new HomeAssistantActionResult { Name = name, EntityId = entityId, Status = "Skipped", Message = "实体未配置或不存在" }); return; } if (!IsAvailable(state)) { results.Add(new HomeAssistantActionResult { Name = name, EntityId = entityId, Status = "Skipped", Message = "设备当前离线" }); return; } var data = new Dictionary { ["entity_id"] = entityId }; if (extraData is not null) { foreach (var item in extraData) data[item.Key] = item.Value; } try { await _homeAssistant.CallServiceAsync(domain, service, data, cancellationToken); results.Add(new HomeAssistantActionResult { Name = name, EntityId = entityId, Status = "Succeeded", Message = "执行成功" }); } catch (Exception exception) { results.Add(new HomeAssistantActionResult { Name = name, EntityId = entityId, Status = "Failed", Message = exception.Message }); } } private long GetCurrentUserId() { var value = _httpContextAccessor.HttpContext?.User.FindFirstValue(HealthUserIdClaim); if (!long.TryParse(value, out var userId)) throw new UnauthorizedAccessException("登录状态已失效"); return userId; } private static HomeAssistantDeviceOutput BuildDevice( IReadOnlyDictionary states, string code, string name, string entityId, string? attributeName = null, string? attributeUnit = null) { if (string.IsNullOrWhiteSpace(entityId) || !states.TryGetValue(entityId, out var state)) { return new HomeAssistantDeviceOutput { Code = code, Name = name, EntityId = entityId, State = "not_found", IsAvailable = false }; } var value = attributeName is null ? state.State : state.GetStringAttribute(attributeName); return new HomeAssistantDeviceOutput { Code = code, Name = name, EntityId = entityId, State = state.State, Value = value, Unit = attributeUnit ?? state.GetStringAttribute("unit_of_measurement"), IsAvailable = IsAvailable(state), LastUpdated = state.LastUpdated }; } private static bool IsAvailable(HomeAssistantState state) => state.State is not ("unavailable" or "unknown"); }