111111
This commit is contained in:
@@ -0,0 +1,279 @@
|
||||
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<HealthSceneExecution> _executionRepository;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public HealthHomeAssistantService(
|
||||
HomeAssistantClient homeAssistant,
|
||||
IOptions<HomeAssistantOptions> options,
|
||||
SqlSugarRepository<HealthSceneExecution> executionRepository,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_homeAssistant = homeAssistant;
|
||||
_options = options.Value;
|
||||
_executionRepository = executionRepository;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[HttpGet("/api/health/home-assistant/status")]
|
||||
public async Task<HomeAssistantStatusOutput> 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<HomeAssistantSceneOutput> ExecuteSleepScene(CancellationToken cancellationToken)
|
||||
=> ExecuteScene("sleep", "舒睡模式", ExecuteSleepActions, cancellationToken);
|
||||
|
||||
[Authorize]
|
||||
[HttpPost("/api/health/home-assistant/scenes/all-off")]
|
||||
public Task<HomeAssistantSceneOutput> ExecuteAllOffScene(CancellationToken cancellationToken)
|
||||
=> ExecuteScene("all-off", "一键关闭", ExecuteAllOffActions, cancellationToken);
|
||||
|
||||
private async Task<HomeAssistantSceneOutput> ExecuteScene(
|
||||
string sceneCode,
|
||||
string sceneName,
|
||||
Func<IReadOnlyDictionary<string, HomeAssistantState>, List<HomeAssistantActionResult>, CancellationToken, Task> executor,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var userId = GetCurrentUserId();
|
||||
var actions = new List<HomeAssistantActionResult>();
|
||||
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<string, HomeAssistantState> states,
|
||||
List<HomeAssistantActionResult> 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<string, object?> { ["humidity"] = scene.TargetHumidity }, cancellationToken);
|
||||
await RunAction(states, results, "切换睡眠模式", entities.Dehumidifier, "humidifier", "set_mode",
|
||||
new Dictionary<string, object?> { ["mode"] = scene.DehumidifierMode }, 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);
|
||||
|
||||
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<string, object?> { ["brightness_pct"] = scene.LightBrightnessPercent }, cancellationToken);
|
||||
}
|
||||
|
||||
await RunAction(states, results, "开启助眠灯光", entities.AmbientLight, "light", "turn_on",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["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<string, HomeAssistantState> states,
|
||||
List<HomeAssistantActionResult> 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<string, HomeAssistantState> states,
|
||||
List<HomeAssistantActionResult> results,
|
||||
string name,
|
||||
string entityId,
|
||||
string domain,
|
||||
string service,
|
||||
IReadOnlyDictionary<string, object?>? 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<string, object?> { ["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<string, HomeAssistantState> 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");
|
||||
}
|
||||
Reference in New Issue
Block a user