111111
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
using Admin.NET.Core;
|
||||
using AspNetCoreRateLimit;
|
||||
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||
using Furion;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
@@ -36,6 +37,7 @@ public static class ProjectOptions
|
||||
services.AddConfigurableOptions<CDConfigOptions>();
|
||||
services.AddConfigurableOptions<DeepSeekOptions>();
|
||||
services.AddConfigurableOptions<LocalizationSettingsOptions>();
|
||||
services.AddConfigurableOptions<HomeAssistantOptions>();
|
||||
services.Configure<IpRateLimitOptions>(App.Configuration.GetSection("IpRateLimiting"));
|
||||
services.Configure<IpRateLimitPolicies>(App.Configuration.GetSection("IpRateLimitPolicies"));
|
||||
services.Configure<ClientRateLimitOptions>(App.Configuration.GetSection("ClientRateLimiting"));
|
||||
|
||||
@@ -8,6 +8,7 @@ using Admin.NET.Core;
|
||||
using Admin.NET.Core.ElasticSearch;
|
||||
using Admin.NET.Core.Service;
|
||||
using AspNetCoreRateLimit;
|
||||
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||
using Furion;
|
||||
using Furion.Logging;
|
||||
using Furion.SpecificationDocument;
|
||||
@@ -74,6 +75,7 @@ public class Startup : AppStartup
|
||||
services.AddCorsAccessor();
|
||||
// 远程请求
|
||||
services.AddHttpRemote();
|
||||
services.AddHttpClient<HomeAssistantClient>();
|
||||
// 任务队列
|
||||
services.AddTaskQueue();
|
||||
// 脱敏检测
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"HomeAssistant": {
|
||||
"Enabled": true,
|
||||
"BaseUrl": "http://192.168.2.2:8123",
|
||||
"TimeoutSeconds": 15,
|
||||
"Entities": {
|
||||
"Dehumidifier": "humidifier.xiaomi_cn_972783525_60l",
|
||||
"DehumidifierFanLevel": "select.xiaomi_cn_972783525_60l_fan_level_p_2_4",
|
||||
"DehumidifierUv": "switch.xiaomi_cn_972783525_60l_uv_p_2_10",
|
||||
"FragranceSwitch": "switch.xiaomi_cn_1206553658_xw002_on_p_2_2",
|
||||
"FragranceLight": "light.xiaomi_cn_1206553658_xw002_s_3_ambient_light",
|
||||
"AmbientLight": "light.lemesh_cn_2041198316_wy0d02_s_2_light",
|
||||
"Speaker": "media_player.xiaomi_cn_1221084723_l17a",
|
||||
"SpeakerSleepMode": "switch.xiaomi_cn_1221084723_l17a_sleep_mode_p_7_3",
|
||||
"Temperature": "sensor.mirsz_cn_2027039858_et71_temperature_p_3_1",
|
||||
"Humidity": "sensor.mirsz_cn_2027039858_et71_relative_humidity_p_3_2",
|
||||
"Pm25": "sensor.mirsz_cn_2027039858_et71_pm2_5_density_p_3_3",
|
||||
"Co2": "sensor.mirsz_cn_2027039858_et71_co2_density_p_3_6",
|
||||
"Formaldehyde": "sensor.mirsz_cn_2027039858_et71_hcho_density_p_3_7",
|
||||
"Tvoc": "sensor.mirsz_cn_2027039858_et71_tvoc_density_p_3_8"
|
||||
},
|
||||
"SleepScene": {
|
||||
"TargetHumidity": 55,
|
||||
"DehumidifierMode": "睡眠",
|
||||
"FanLevel": "1挡",
|
||||
"LightBrightnessPercent": 20,
|
||||
"LightColorTemperatureKelvin": 2700,
|
||||
"EnableFragrance": true,
|
||||
"EnableSpeakerSleepMode": false,
|
||||
"EnableUv": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||
|
||||
public sealed class HomeAssistantClient
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly HomeAssistantOptions _options;
|
||||
|
||||
public HomeAssistantClient(HttpClient httpClient, IOptions<HomeAssistantOptions> options)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_options = options.Value;
|
||||
_httpClient.Timeout = TimeSpan.FromSeconds(Math.Clamp(_options.TimeoutSeconds, 3, 60));
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<HomeAssistantState>> GetStatesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = CreateRequest(HttpMethod.Get, "api/states");
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
await EnsureSuccessAsync(response, cancellationToken);
|
||||
return await response.Content.ReadFromJsonAsync<List<HomeAssistantState>>(cancellationToken: cancellationToken) ?? [];
|
||||
}
|
||||
|
||||
public async Task CallServiceAsync(
|
||||
string domain,
|
||||
string service,
|
||||
IReadOnlyDictionary<string, object?> data,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
using var request = CreateRequest(HttpMethod.Post, $"api/services/{domain}/{service}");
|
||||
request.Content = JsonContent.Create(data);
|
||||
using var response = await _httpClient.SendAsync(request, cancellationToken);
|
||||
await EnsureSuccessAsync(response, cancellationToken);
|
||||
}
|
||||
|
||||
private HttpRequestMessage CreateRequest(HttpMethod method, string path)
|
||||
{
|
||||
if (!_options.Enabled)
|
||||
throw new InvalidOperationException("Home Assistant 接入未启用");
|
||||
if (string.IsNullOrWhiteSpace(_options.BaseUrl))
|
||||
throw new InvalidOperationException("Home Assistant 地址未配置");
|
||||
if (string.IsNullOrWhiteSpace(_options.AccessToken))
|
||||
throw new InvalidOperationException("Home Assistant 访问令牌未配置");
|
||||
|
||||
var baseUrl = _options.BaseUrl.TrimEnd('/') + "/";
|
||||
var request = new HttpRequestMessage(method, new Uri(new Uri(baseUrl), path));
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _options.AccessToken);
|
||||
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
|
||||
return request;
|
||||
}
|
||||
|
||||
private static async Task EnsureSuccessAsync(HttpResponseMessage response, CancellationToken cancellationToken)
|
||||
{
|
||||
if (response.IsSuccessStatusCode) return;
|
||||
var body = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
var message = body.Length > 300 ? body[..300] : body;
|
||||
throw new HttpRequestException($"Home Assistant 返回 {(int)response.StatusCode}: {message}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||
|
||||
public sealed class HomeAssistantState
|
||||
{
|
||||
[JsonPropertyName("entity_id")]
|
||||
public string EntityId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("state")]
|
||||
public string State { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("attributes")]
|
||||
public JsonElement Attributes { get; set; }
|
||||
|
||||
[JsonPropertyName("last_updated")]
|
||||
public DateTimeOffset? LastUpdated { get; set; }
|
||||
|
||||
public string? GetStringAttribute(string name)
|
||||
{
|
||||
if (!Attributes.TryGetProperty(name, out var value)) return null;
|
||||
return value.ValueKind == JsonValueKind.String ? value.GetString() : value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class HomeAssistantActionResult
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string EntityId { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using Furion.ConfigurableOptions;
|
||||
|
||||
namespace Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||
|
||||
public sealed class HomeAssistantOptions : IConfigurableOptions
|
||||
{
|
||||
public bool Enabled { get; set; } = true;
|
||||
public string BaseUrl { get; set; } = string.Empty;
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public int TimeoutSeconds { get; set; } = 15;
|
||||
public HomeAssistantEntityOptions Entities { get; set; } = new();
|
||||
public HomeAssistantSleepSceneOptions SleepScene { get; set; } = new();
|
||||
}
|
||||
|
||||
public sealed class HomeAssistantEntityOptions
|
||||
{
|
||||
public string Dehumidifier { get; set; } = string.Empty;
|
||||
public string DehumidifierFanLevel { get; set; } = string.Empty;
|
||||
public string DehumidifierUv { get; set; } = string.Empty;
|
||||
public string FragranceSwitch { get; set; } = string.Empty;
|
||||
public string FragranceLight { get; set; } = string.Empty;
|
||||
public string AmbientLight { get; set; } = string.Empty;
|
||||
public string Speaker { get; set; } = string.Empty;
|
||||
public string SpeakerSleepMode { get; set; } = string.Empty;
|
||||
public string Temperature { get; set; } = string.Empty;
|
||||
public string Humidity { get; set; } = string.Empty;
|
||||
public string Pm25 { get; set; } = string.Empty;
|
||||
public string Co2 { get; set; } = string.Empty;
|
||||
public string Formaldehyde { get; set; } = string.Empty;
|
||||
public string Tvoc { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public sealed class HomeAssistantSleepSceneOptions
|
||||
{
|
||||
public int TargetHumidity { get; set; } = 55;
|
||||
public string DehumidifierMode { get; set; } = "睡眠";
|
||||
public string FanLevel { get; set; } = "1挡";
|
||||
public int LightBrightnessPercent { get; set; } = 20;
|
||||
public int LightColorTemperatureKelvin { get; set; } = 2700;
|
||||
public bool EnableFragrance { get; set; } = true;
|
||||
public bool EnableSpeakerSleepMode { get; set; }
|
||||
public bool EnableUv { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
namespace Evergreen.Health.Application.Health.Models;
|
||||
|
||||
public sealed class HomeAssistantDeviceOutput
|
||||
{
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string EntityId { get; set; } = string.Empty;
|
||||
public string State { get; set; } = string.Empty;
|
||||
public string? Value { get; set; }
|
||||
public string? Unit { get; set; }
|
||||
public bool IsAvailable { get; set; }
|
||||
public DateTimeOffset? LastUpdated { get; set; }
|
||||
}
|
||||
|
||||
public sealed class HomeAssistantStatusOutput
|
||||
{
|
||||
public bool Connected { get; set; }
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public DateTimeOffset CheckedAt { get; set; } = DateTimeOffset.Now;
|
||||
public List<HomeAssistantDeviceOutput> Devices { get; set; } = [];
|
||||
public List<HomeAssistantDeviceOutput> Environment { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class HomeAssistantSceneOutput
|
||||
{
|
||||
public string SceneCode { get; set; } = string.Empty;
|
||||
public string SceneName { get; set; } = string.Empty;
|
||||
public bool Success { get; set; }
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
public DateTimeOffset ExecutedAt { get; set; } = DateTimeOffset.Now;
|
||||
public List<HomeAssistantSceneActionOutput> Actions { get; set; } = [];
|
||||
}
|
||||
|
||||
public sealed class HomeAssistantSceneActionOutput
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string EntityId { get; set; } = string.Empty;
|
||||
public string Status { get; set; } = string.Empty;
|
||||
public string Message { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -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