1111
This commit is contained in:
@@ -76,6 +76,8 @@ public class Startup : AppStartup
|
|||||||
// 远程请求
|
// 远程请求
|
||||||
services.AddHttpRemote();
|
services.AddHttpRemote();
|
||||||
services.AddHttpClient<HomeAssistantClient>();
|
services.AddHttpClient<HomeAssistantClient>();
|
||||||
|
services.AddSingleton<HomeAssistantLightTransitionService>();
|
||||||
|
services.AddHostedService(provider => provider.GetRequiredService<HomeAssistantLightTransitionService>());
|
||||||
// 任务队列
|
// 任务队列
|
||||||
services.AddTaskQueue();
|
services.AddTaskQueue();
|
||||||
// 脱敏检测
|
// 脱敏检测
|
||||||
|
|||||||
@@ -9,6 +9,12 @@
|
|||||||
"DehumidifierUv": "switch.xiaomi_cn_972783525_60l_uv_p_2_10",
|
"DehumidifierUv": "switch.xiaomi_cn_972783525_60l_uv_p_2_10",
|
||||||
"FragranceSwitch": "switch.xiaomi_cn_1206553658_xw002_on_p_2_2",
|
"FragranceSwitch": "switch.xiaomi_cn_1206553658_xw002_on_p_2_2",
|
||||||
"FragranceLight": "light.xiaomi_cn_1206553658_xw002_s_3_ambient_light",
|
"FragranceLight": "light.xiaomi_cn_1206553658_xw002_s_3_ambient_light",
|
||||||
|
"FragranceDuration": "number.xiaomi_cn_1206553658_xw002_fragrance_out_time_p_2_3",
|
||||||
|
"FragranceInterval": "number.xiaomi_cn_1206553658_xw002_fragrance_out_time_p_2_4",
|
||||||
|
"FragranceQuickRelease": "button.xiaomi_cn_1206553658_xw002_fragrance_delivery_a_4_1",
|
||||||
|
"FragranceAutoRelease": "switch.xiaomi_cn_1206553658_xw002_auto_fragrance_p_4_1",
|
||||||
|
"FragranceAutoLight": "switch.xiaomi_cn_1206553658_xw002_auto_light_p_4_2",
|
||||||
|
"FragranceLightAutoOff": "switch.xiaomi_cn_1206553658_xw002_light_auto_off_p_4_3",
|
||||||
"AmbientLight": "light.lemesh_cn_2041198316_wy0d02_s_2_light",
|
"AmbientLight": "light.lemesh_cn_2041198316_wy0d02_s_2_light",
|
||||||
"Speaker": "media_player.xiaomi_cn_1221084723_l17a",
|
"Speaker": "media_player.xiaomi_cn_1221084723_l17a",
|
||||||
"SpeakerSleepMode": "switch.xiaomi_cn_1221084723_l17a_sleep_mode_p_7_3",
|
"SpeakerSleepMode": "switch.xiaomi_cn_1221084723_l17a_sleep_mode_p_7_3",
|
||||||
|
|||||||
+233
@@ -0,0 +1,233 @@
|
|||||||
|
using Evergreen.Health.Application.Health.Models;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||||
|
|
||||||
|
/// <summary>在服务端驱动卧室灯光渐变,浏览器关闭后任务仍继续。</summary>
|
||||||
|
public sealed class HomeAssistantLightTransitionService : IHostedService
|
||||||
|
{
|
||||||
|
private const int SleepStartPercent = 50;
|
||||||
|
private const int WakeTargetPercent = 70;
|
||||||
|
private static readonly TimeSpan StepInterval = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
private readonly HomeAssistantClient _homeAssistant;
|
||||||
|
private readonly HomeAssistantOptions _options;
|
||||||
|
private readonly ILogger<HomeAssistantLightTransitionService> _logger;
|
||||||
|
private readonly SemaphoreSlim _operationLock = new(1, 1);
|
||||||
|
private readonly object _statusLock = new();
|
||||||
|
private CancellationTokenSource? _activeCancellation;
|
||||||
|
private Task? _activeTask;
|
||||||
|
private HomeAssistantLightTransitionOutput _status = new();
|
||||||
|
|
||||||
|
public HomeAssistantLightTransitionService(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IOptions<HomeAssistantOptions> options,
|
||||||
|
ILogger<HomeAssistantLightTransitionService> logger)
|
||||||
|
{
|
||||||
|
_options = options.Value;
|
||||||
|
_logger = logger;
|
||||||
|
_homeAssistant = new HomeAssistantClient(httpClientFactory.CreateClient(nameof(HomeAssistantLightTransitionService)), options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
public async Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _operationLock.WaitAsync(cancellationToken);
|
||||||
|
try { await StopCurrentAsync(); }
|
||||||
|
finally { _operationLock.Release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public HomeAssistantLightTransitionOutput GetStatus()
|
||||||
|
{
|
||||||
|
lock (_statusLock)
|
||||||
|
{
|
||||||
|
return new HomeAssistantLightTransitionOutput
|
||||||
|
{
|
||||||
|
Success = _status.Success,
|
||||||
|
Running = _status.Running,
|
||||||
|
Mode = _status.Mode,
|
||||||
|
BrightnessPercent = _status.BrightnessPercent,
|
||||||
|
TargetPercent = _status.TargetPercent,
|
||||||
|
Message = _status.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HomeAssistantLightTransitionOutput> StartSleepAsync(int colorTemperatureKelvin, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _operationLock.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await StopCurrentAsync();
|
||||||
|
await ReadAvailableLightAsync(cancellationToken);
|
||||||
|
await _homeAssistant.CallServiceAsync("light", "turn_on", new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["entity_id"] = LightEntityId,
|
||||||
|
["brightness_pct"] = SleepStartPercent,
|
||||||
|
["color_temp_kelvin"] = Math.Clamp(colorTemperatureKelvin, 2200, 5000)
|
||||||
|
}, cancellationToken);
|
||||||
|
|
||||||
|
SetStatus(true, true, "sleep", SleepStartPercent, 0, "助眠渐暗已启动,每5秒降低1%亮度");
|
||||||
|
Launch("sleep", SleepStartPercent);
|
||||||
|
return GetStatus();
|
||||||
|
}
|
||||||
|
finally { _operationLock.Release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HomeAssistantLightTransitionOutput> StartWakeAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _operationLock.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await StopCurrentAsync();
|
||||||
|
var light = await ReadAvailableLightAsync(cancellationToken);
|
||||||
|
var isOff = string.Equals(light.State, "off", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var brightness = light.GetDoubleAttribute("brightness");
|
||||||
|
if (!isOff && brightness is null)
|
||||||
|
throw new InvalidOperationException("灯具未返回当前亮度,无法从当前亮度启动晨起唤醒");
|
||||||
|
var current = isOff ? 1 : Math.Clamp((int)Math.Round(brightness!.Value / 255d * 100), 1, 100);
|
||||||
|
|
||||||
|
if (current >= WakeTargetPercent && !isOff)
|
||||||
|
{
|
||||||
|
SetStatus(true, false, "wake", current, WakeTargetPercent, "当前亮度已达到70%或更高,保持原亮度");
|
||||||
|
return GetStatus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOff)
|
||||||
|
{
|
||||||
|
await _homeAssistant.CallServiceAsync("light", "turn_on", new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["entity_id"] = LightEntityId,
|
||||||
|
["brightness_pct"] = 1
|
||||||
|
}, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetStatus(true, true, "wake", current, WakeTargetPercent, "晨起唤醒已启动,每5秒增加1%亮度");
|
||||||
|
Launch("wake", current);
|
||||||
|
return GetStatus();
|
||||||
|
}
|
||||||
|
finally { _operationLock.Release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<HomeAssistantLightTransitionOutput> CancelAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await _operationLock.WaitAsync(cancellationToken);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await StopCurrentAsync();
|
||||||
|
var brightness = GetStatus().BrightnessPercent;
|
||||||
|
SetStatus(true, false, "none", brightness, 0, "灯光渐变已停止");
|
||||||
|
return GetStatus();
|
||||||
|
}
|
||||||
|
finally { _operationLock.Release(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private string LightEntityId => !string.IsNullOrWhiteSpace(_options.Entities.AmbientLight)
|
||||||
|
? _options.Entities.AmbientLight
|
||||||
|
: throw new InvalidOperationException("卧室灯光实体尚未配置");
|
||||||
|
|
||||||
|
private async Task<HomeAssistantState> ReadAvailableLightAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var light = (await _homeAssistant.GetStatesAsync(cancellationToken))
|
||||||
|
.FirstOrDefault(item => string.Equals(item.EntityId, LightEntityId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (light is null || light.State is "unknown" or "unavailable")
|
||||||
|
throw new InvalidOperationException("卧室灯光当前离线或不可用");
|
||||||
|
return light;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Launch(string mode, int initialPercent)
|
||||||
|
{
|
||||||
|
_activeCancellation = new CancellationTokenSource();
|
||||||
|
_activeTask = RunAsync(mode, initialPercent, _activeCancellation.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RunAsync(string mode, int initialPercent, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var current = initialPercent;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
await Task.Delay(StepInterval, cancellationToken);
|
||||||
|
var light = await ReadAvailableLightAsync(cancellationToken);
|
||||||
|
if (!string.Equals(light.State, "on", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
SetStatus(false, false, mode, 0, mode == "wake" ? WakeTargetPercent : 0, "灯已从其他位置关闭,渐变已停止");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var brightness = light.GetDoubleAttribute("brightness");
|
||||||
|
if (brightness is null)
|
||||||
|
{
|
||||||
|
SetStatus(false, false, mode, current, mode == "wake" ? WakeTargetPercent : 0, "灯具未返回当前亮度,渐变已停止");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var observed = Math.Clamp((int)Math.Round(brightness.Value / 255d * 100), 1, 100);
|
||||||
|
if (Math.Abs(observed - current) > 2)
|
||||||
|
{
|
||||||
|
SetStatus(false, false, mode, observed, mode == "wake" ? WakeTargetPercent : 0, "灯光亮度已从其他位置修改,渐变已停止");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode == "sleep" && current == 1)
|
||||||
|
{
|
||||||
|
await _homeAssistant.CallServiceAsync("light", "turn_off", new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["entity_id"] = LightEntityId
|
||||||
|
}, cancellationToken);
|
||||||
|
SetStatus(true, false, mode, 0, 0, "助眠渐暗已完成,灯光已关闭");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var next = mode == "sleep" ? current - 1 : current + 1;
|
||||||
|
await _homeAssistant.CallServiceAsync("light", "turn_on", new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["entity_id"] = LightEntityId,
|
||||||
|
["brightness_pct"] = next
|
||||||
|
}, cancellationToken);
|
||||||
|
current = next;
|
||||||
|
var finished = mode == "wake" && current >= WakeTargetPercent;
|
||||||
|
SetStatus(true, !finished, mode, current, mode == "wake" ? WakeTargetPercent : 0,
|
||||||
|
finished ? "晨起唤醒已完成,灯光保持70%亮度" : mode == "wake" ? "晨起唤醒进行中" : "助眠渐暗进行中");
|
||||||
|
if (finished) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { }
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
_logger.LogWarning(exception, "卧室灯光渐变任务中断");
|
||||||
|
SetStatus(false, false, mode, current, mode == "wake" ? WakeTargetPercent : 0,
|
||||||
|
$"灯光渐变中断:{exception.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StopCurrentAsync()
|
||||||
|
{
|
||||||
|
if (_activeCancellation is null) return;
|
||||||
|
_activeCancellation.Cancel();
|
||||||
|
if (_activeTask is not null) await _activeTask;
|
||||||
|
_activeCancellation.Dispose();
|
||||||
|
_activeCancellation = null;
|
||||||
|
_activeTask = null;
|
||||||
|
SetStatus(true, false, "none", GetStatus().BrightnessPercent, 0, "灯光渐变已停止");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetStatus(bool success, bool running, string mode, int brightness, int target, string message)
|
||||||
|
{
|
||||||
|
lock (_statusLock)
|
||||||
|
{
|
||||||
|
_status = new HomeAssistantLightTransitionOutput
|
||||||
|
{
|
||||||
|
Success = success,
|
||||||
|
Running = running,
|
||||||
|
Mode = mode,
|
||||||
|
BrightnessPercent = brightness,
|
||||||
|
TargetPercent = target,
|
||||||
|
Message = message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,12 @@ public sealed class HomeAssistantEntityOptions
|
|||||||
public string DehumidifierUv { get; set; } = string.Empty;
|
public string DehumidifierUv { get; set; } = string.Empty;
|
||||||
public string FragranceSwitch { get; set; } = string.Empty;
|
public string FragranceSwitch { get; set; } = string.Empty;
|
||||||
public string FragranceLight { get; set; } = string.Empty;
|
public string FragranceLight { get; set; } = string.Empty;
|
||||||
|
public string FragranceDuration { get; set; } = string.Empty;
|
||||||
|
public string FragranceInterval { get; set; } = string.Empty;
|
||||||
|
public string FragranceQuickRelease { get; set; } = string.Empty;
|
||||||
|
public string FragranceAutoRelease { get; set; } = string.Empty;
|
||||||
|
public string FragranceAutoLight { get; set; } = string.Empty;
|
||||||
|
public string FragranceLightAutoOff { get; set; } = string.Empty;
|
||||||
public string AmbientLight { get; set; } = string.Empty;
|
public string AmbientLight { get; set; } = string.Empty;
|
||||||
public string Speaker { get; set; } = string.Empty;
|
public string Speaker { get; set; } = string.Empty;
|
||||||
public string SpeakerSleepMode { get; set; } = string.Empty;
|
public string SpeakerSleepMode { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -113,6 +113,25 @@ public sealed class HomeAssistantDeviceSwitchOutput
|
|||||||
public string Message { get; set; } = string.Empty;
|
public string Message { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class HomeAssistantLightTransitionOutput
|
||||||
|
{
|
||||||
|
public bool Success { get; set; } = true;
|
||||||
|
public bool Running { get; set; }
|
||||||
|
public string Mode { get; set; } = "none";
|
||||||
|
public int BrightnessPercent { get; set; }
|
||||||
|
public int TargetPercent { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class HomeAssistantSpeakerDemoOutput
|
||||||
|
{
|
||||||
|
public string AudioUrl { get; set; } = string.Empty;
|
||||||
|
public bool SpeakerOnline { get; set; }
|
||||||
|
public bool DirectPlaybackSupported { get; set; }
|
||||||
|
public bool CommandAccepted { get; set; }
|
||||||
|
public string Message { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class HomeAssistantSetParameterInput
|
public sealed class HomeAssistantSetParameterInput
|
||||||
{
|
{
|
||||||
public string DeviceId { get; set; } = string.Empty;
|
public string DeviceId { get; set; } = string.Empty;
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ namespace Evergreen.Health.Application.Health.Services;
|
|||||||
public sealed class HealthHomeAssistantService : IDynamicApiController, ITransient
|
public sealed class HealthHomeAssistantService : IDynamicApiController, ITransient
|
||||||
{
|
{
|
||||||
private const string HealthUserIdClaim = "health_user_id";
|
private const string HealthUserIdClaim = "health_user_id";
|
||||||
|
private const int PlayMediaFeature = 512;
|
||||||
|
private const string DemoAudioUrl = "https://oss.myzxjgt.com/App.Live/uploads/%E9%9F%B3%E9%A2%91/2026-05-29/57a6a64d8dfa4a1bb876a15bb8fdfd24.mp3";
|
||||||
private readonly HomeAssistantClient _homeAssistant;
|
private readonly HomeAssistantClient _homeAssistant;
|
||||||
|
private readonly HomeAssistantLightTransitionService _lightTransition;
|
||||||
private readonly HomeAssistantRegistryService _registryService;
|
private readonly HomeAssistantRegistryService _registryService;
|
||||||
private readonly HomeAssistantOptions _options;
|
private readonly HomeAssistantOptions _options;
|
||||||
private readonly SqlSugarRepository<HealthSceneExecution> _executionRepository;
|
private readonly SqlSugarRepository<HealthSceneExecution> _executionRepository;
|
||||||
@@ -23,6 +26,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
|
|
||||||
public HealthHomeAssistantService(
|
public HealthHomeAssistantService(
|
||||||
HomeAssistantClient homeAssistant,
|
HomeAssistantClient homeAssistant,
|
||||||
|
HomeAssistantLightTransitionService lightTransition,
|
||||||
HomeAssistantRegistryService registryService,
|
HomeAssistantRegistryService registryService,
|
||||||
IOptions<HomeAssistantOptions> options,
|
IOptions<HomeAssistantOptions> options,
|
||||||
SqlSugarRepository<HealthSceneExecution> executionRepository,
|
SqlSugarRepository<HealthSceneExecution> executionRepository,
|
||||||
@@ -30,6 +34,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
IHttpContextAccessor httpContextAccessor)
|
IHttpContextAccessor httpContextAccessor)
|
||||||
{
|
{
|
||||||
_homeAssistant = homeAssistant;
|
_homeAssistant = homeAssistant;
|
||||||
|
_lightTransition = lightTransition;
|
||||||
_registryService = registryService;
|
_registryService = registryService;
|
||||||
_options = options.Value;
|
_options = options.Value;
|
||||||
_executionRepository = executionRepository;
|
_executionRepository = executionRepository;
|
||||||
@@ -37,6 +42,97 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("/api/health/home-assistant/lights/transition")]
|
||||||
|
public HomeAssistantLightTransitionOutput GetLightTransitionStatus() => _lightTransition.GetStatus();
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost("/api/health/home-assistant/lights/sleep")]
|
||||||
|
public async Task<HomeAssistantLightTransitionOutput> StartSleepLight(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try { return await _lightTransition.StartSleepAsync(_options.SleepScene.LightColorTemperatureKelvin, cancellationToken); }
|
||||||
|
catch (Exception exception) { return new HomeAssistantLightTransitionOutput { Success = false, Message = exception.Message }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost("/api/health/home-assistant/lights/wake")]
|
||||||
|
public async Task<HomeAssistantLightTransitionOutput> StartWakeLight(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try { return await _lightTransition.StartWakeAsync(cancellationToken); }
|
||||||
|
catch (Exception exception) { return new HomeAssistantLightTransitionOutput { Success = false, Message = exception.Message }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost("/api/health/home-assistant/lights/stop")]
|
||||||
|
public async Task<HomeAssistantLightTransitionOutput> StopLightTransition(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try { return await _lightTransition.CancelAsync(cancellationToken); }
|
||||||
|
catch (Exception exception) { return new HomeAssistantLightTransitionOutput { Success = false, Message = exception.Message }; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpGet("/api/health/home-assistant/speaker/demo")]
|
||||||
|
public async Task<HomeAssistantSpeakerDemoOutput> GetSpeakerDemo(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = new HomeAssistantSpeakerDemoOutput { AudioUrl = DemoAudioUrl };
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var entityId = _options.Entities.Speaker;
|
||||||
|
if (string.IsNullOrWhiteSpace(entityId))
|
||||||
|
{
|
||||||
|
result.Message = "尚未配置音箱实体";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = (await _homeAssistant.GetStatesAsync(cancellationToken))
|
||||||
|
.FirstOrDefault(item => string.Equals(item.EntityId, entityId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
result.SpeakerOnline = state is not null && IsAvailable(state);
|
||||||
|
if (!result.SpeakerOnline)
|
||||||
|
{
|
||||||
|
result.Message = "音箱当前离线或不可用";
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
var features = (long)(state!.GetDoubleAttribute("supported_features") ?? 0);
|
||||||
|
result.DirectPlaybackSupported = (features & PlayMediaFeature) != 0;
|
||||||
|
result.Message = result.DirectPlaybackSupported
|
||||||
|
? "音箱声明支持指定音频播放,可以尝试 OSS 示例音频"
|
||||||
|
: "当前 Xiaomi Home 音箱实体不支持播放指定音频;OSS 链接可在浏览器试听,但暂不能投放到音箱";
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
result.Message = $"音箱能力检测失败:{exception.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpPost("/api/health/home-assistant/speaker/play-demo")]
|
||||||
|
public async Task<HomeAssistantSpeakerDemoOutput> PlaySpeakerDemo(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await GetSpeakerDemo(cancellationToken);
|
||||||
|
if (!result.SpeakerOnline || !result.DirectPlaybackSupported) return result;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _homeAssistant.CallServiceAsync("media_player", "play_media", new Dictionary<string, object?>
|
||||||
|
{
|
||||||
|
["entity_id"] = _options.Entities.Speaker,
|
||||||
|
["media_content_id"] = DemoAudioUrl,
|
||||||
|
["media_content_type"] = "music"
|
||||||
|
}, cancellationToken);
|
||||||
|
result.CommandAccepted = true;
|
||||||
|
result.Message = "播放指令已发送,请确认音箱是否实际出声;接口接受不等于播放成功";
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
result.Message = $"音箱未接受播放指令:{exception.Message}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpGet("/api/health/home-assistant/status")]
|
[HttpGet("/api/health/home-assistant/status")]
|
||||||
public async Task<HomeAssistantStatusOutput> GetStatus(CancellationToken cancellationToken)
|
public async Task<HomeAssistantStatusOutput> GetStatus(CancellationToken cancellationToken)
|
||||||
@@ -172,6 +268,24 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (domain == "light" && string.Equals(targetEntityId, _options.Entities.AmbientLight, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (input.TurnOn)
|
||||||
|
{
|
||||||
|
var transition = await _lightTransition.StartSleepAsync(2700, cancellationToken);
|
||||||
|
return new HomeAssistantDeviceSwitchOutput
|
||||||
|
{
|
||||||
|
Success = transition.Success,
|
||||||
|
DeviceId = input.DeviceId,
|
||||||
|
EntityId = targetEntityId,
|
||||||
|
State = "on",
|
||||||
|
Message = transition.Message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await _lightTransition.CancelAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
var targetOn = input.TurnOn;
|
var targetOn = input.TurnOn;
|
||||||
bool? targetMuted = null;
|
bool? targetMuted = null;
|
||||||
var service = targetOn ? "turn_on" : "turn_off";
|
var service = targetOn ? "turn_on" : "turn_off";
|
||||||
@@ -354,6 +468,45 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
domain = "switch";
|
domain = "switch";
|
||||||
service = ReadBool(input.Value) ? "turn_on" : "turn_off";
|
service = ReadBool(input.Value) ? "turn_on" : "turn_off";
|
||||||
break;
|
break;
|
||||||
|
case "fragrance_duration":
|
||||||
|
targetEntityId = _options.Entities.FragranceDuration;
|
||||||
|
domain = "number";
|
||||||
|
service = "set_value";
|
||||||
|
serviceData["value"] = ReadInt(input.Value, 2, 6, "出香时长");
|
||||||
|
break;
|
||||||
|
case "fragrance_interval":
|
||||||
|
targetEntityId = _options.Entities.FragranceInterval;
|
||||||
|
domain = "number";
|
||||||
|
service = "set_value";
|
||||||
|
var fragranceInterval = ReadInt(input.Value, 10, 20, "出香间隔");
|
||||||
|
if (fragranceInterval % 5 != 0) throw new InvalidOperationException("出香间隔仅支持 10、15 或 20 分钟");
|
||||||
|
serviceData["value"] = fragranceInterval;
|
||||||
|
break;
|
||||||
|
case "fragrance_light":
|
||||||
|
targetEntityId = _options.Entities.FragranceLight;
|
||||||
|
domain = "light";
|
||||||
|
service = ReadBool(input.Value) ? "turn_on" : "turn_off";
|
||||||
|
break;
|
||||||
|
case "fragrance_quick_release":
|
||||||
|
targetEntityId = _options.Entities.FragranceQuickRelease;
|
||||||
|
domain = "button";
|
||||||
|
service = "press";
|
||||||
|
break;
|
||||||
|
case "fragrance_auto_release":
|
||||||
|
targetEntityId = _options.Entities.FragranceAutoRelease;
|
||||||
|
domain = "switch";
|
||||||
|
service = ReadBool(input.Value) ? "turn_on" : "turn_off";
|
||||||
|
break;
|
||||||
|
case "fragrance_auto_light":
|
||||||
|
targetEntityId = _options.Entities.FragranceAutoLight;
|
||||||
|
domain = "switch";
|
||||||
|
service = ReadBool(input.Value) ? "turn_on" : "turn_off";
|
||||||
|
break;
|
||||||
|
case "fragrance_light_auto_off":
|
||||||
|
targetEntityId = _options.Entities.FragranceLightAutoOff;
|
||||||
|
domain = "switch";
|
||||||
|
service = ReadBool(input.Value) ? "turn_on" : "turn_off";
|
||||||
|
break;
|
||||||
case "mattress_left_heating":
|
case "mattress_left_heating":
|
||||||
targetEntityId = _options.Entities.MattressLeftHeatingLevel;
|
targetEntityId = _options.Entities.MattressLeftHeatingLevel;
|
||||||
domain = "number";
|
domain = "number";
|
||||||
@@ -403,6 +556,9 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
if (!stateMap.TryGetValue(targetEntityId, out var targetState) || !IsAvailable(targetState))
|
if (!stateMap.TryGetValue(targetEntityId, out var targetState) || !IsAvailable(targetState))
|
||||||
throw new InvalidOperationException("目标参数实体当前离线或不可用");
|
throw new InvalidOperationException("目标参数实体当前离线或不可用");
|
||||||
|
|
||||||
|
if (parameterType is "brightness" or "color_temp")
|
||||||
|
await _lightTransition.CancelAsync(cancellationToken);
|
||||||
|
|
||||||
serviceData["entity_id"] = targetEntityId;
|
serviceData["entity_id"] = targetEntityId;
|
||||||
await _homeAssistant.CallServiceAsync(domain, service, serviceData, cancellationToken);
|
await _homeAssistant.CallServiceAsync(domain, service, serviceData, cancellationToken);
|
||||||
|
|
||||||
@@ -442,6 +598,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
|
|
||||||
var successMessage = parameterType switch
|
var successMessage = parameterType switch
|
||||||
{
|
{
|
||||||
|
"fragrance_quick_release" => "快速出香指令已发送",
|
||||||
"mattress_left_mode" when !string.Equals(serviceData["option"]?.ToString(), "无", StringComparison.Ordinal) =>
|
"mattress_left_mode" when !string.Equals(serviceData["option"]?.ToString(), "无", StringComparison.Ordinal) =>
|
||||||
"左侧承托模式已启动;床垫共用气泵,右侧会自动停止",
|
"左侧承托模式已启动;床垫共用气泵,右侧会自动停止",
|
||||||
"mattress_right_mode" when !string.Equals(serviceData["option"]?.ToString(), "无", StringComparison.Ordinal) =>
|
"mattress_right_mode" when !string.Equals(serviceData["option"]?.ToString(), "无", StringComparison.Ordinal) =>
|
||||||
@@ -572,6 +729,9 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
var lightBrightness = customConfig?.LightBrightnessPercent ?? scene.LightBrightnessPercent;
|
var lightBrightness = customConfig?.LightBrightnessPercent ?? scene.LightBrightnessPercent;
|
||||||
var lightColorTemp = customConfig?.LightColorTemperatureKelvin ?? scene.LightColorTemperatureKelvin;
|
var lightColorTemp = customConfig?.LightColorTemperatureKelvin ?? scene.LightColorTemperatureKelvin;
|
||||||
var enableFragrance = customConfig?.EnableFragrance ?? scene.EnableFragrance;
|
var enableFragrance = customConfig?.EnableFragrance ?? scene.EnableFragrance;
|
||||||
|
var fragranceLevel = Math.Clamp(customConfig?.FragranceLevel ?? 1, 1, 3);
|
||||||
|
var fragranceDuration = fragranceLevel switch { 1 => 2, 2 => 4, _ => 6 };
|
||||||
|
var fragranceInterval = fragranceLevel switch { 1 => 20, 2 => 15, _ => 10 };
|
||||||
var enableSpeakerSleepMode = customConfig?.EnableSpeakerSleepMode ?? scene.EnableSpeakerSleepMode;
|
var enableSpeakerSleepMode = customConfig?.EnableSpeakerSleepMode ?? scene.EnableSpeakerSleepMode;
|
||||||
var speakerVolume = customConfig?.SpeakerVolumePercent ?? 15;
|
var speakerVolume = customConfig?.SpeakerVolumePercent ?? 15;
|
||||||
var mattressHeatingLevel = customConfig?.MattressHeatingLevel ?? scene.MattressHeatingLevel;
|
var mattressHeatingLevel = customConfig?.MattressHeatingLevel ?? scene.MattressHeatingLevel;
|
||||||
@@ -590,16 +750,43 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
if (enableFragrance)
|
if (enableFragrance)
|
||||||
{
|
{
|
||||||
await RunAction(states, results, "开启香氛机", entities.FragranceSwitch, "switch", "turn_on", null, cancellationToken);
|
await RunAction(states, results, "开启香氛机", entities.FragranceSwitch, "switch", "turn_on", null, cancellationToken);
|
||||||
|
await RunAction(states, results, "设置出香时长", entities.FragranceDuration, "number", "set_value",
|
||||||
|
new Dictionary<string, object?> { ["value"] = fragranceDuration }, cancellationToken);
|
||||||
|
await RunAction(states, results, "设置出香间隔", entities.FragranceInterval, "number", "set_value",
|
||||||
|
new Dictionary<string, object?> { ["value"] = fragranceInterval }, cancellationToken);
|
||||||
await RunAction(states, results, "开启香氛灯", entities.FragranceLight, "light", "turn_on",
|
await RunAction(states, results, "开启香氛灯", entities.FragranceLight, "light", "turn_on",
|
||||||
new Dictionary<string, object?> { ["brightness_pct"] = lightBrightness }, cancellationToken);
|
new Dictionary<string, object?> { ["brightness_pct"] = lightBrightness }, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
await RunAction(states, results, "开启助眠灯光", entities.AmbientLight, "light", "turn_on",
|
if (string.IsNullOrWhiteSpace(entities.AmbientLight) ||
|
||||||
new Dictionary<string, object?>
|
!states.TryGetValue(entities.AmbientLight, out var lightState) || !IsAvailable(lightState))
|
||||||
{
|
{
|
||||||
["brightness_pct"] = lightBrightness,
|
results.Add(new HomeAssistantActionResult
|
||||||
["color_temp_kelvin"] = lightColorTemp
|
{
|
||||||
}, cancellationToken);
|
Name = "开启助眠渐暗灯光", EntityId = entities.AmbientLight,
|
||||||
|
Status = "Skipped", Message = "设备未配置或离线"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _lightTransition.StartSleepAsync(lightColorTemp, cancellationToken);
|
||||||
|
results.Add(new HomeAssistantActionResult
|
||||||
|
{
|
||||||
|
Name = "开启助眠渐暗灯光", EntityId = entities.AmbientLight,
|
||||||
|
Status = "Succeeded", Message = "已从50%开始,每5秒降低1%,最终关灯"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
results.Add(new HomeAssistantActionResult
|
||||||
|
{
|
||||||
|
Name = "开启助眠渐暗灯光", EntityId = entities.AmbientLight,
|
||||||
|
Status = "Failed", Message = exception.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (enableSpeakerSleepMode)
|
if (enableSpeakerSleepMode)
|
||||||
{
|
{
|
||||||
@@ -635,6 +822,7 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var entities = _options.Entities;
|
var entities = _options.Entities;
|
||||||
|
await _lightTransition.CancelAsync(cancellationToken);
|
||||||
await RunAction(states, results, "关闭除湿机", entities.Dehumidifier, "humidifier", "turn_off", null, cancellationToken);
|
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.FragranceSwitch, "switch", "turn_off", null, cancellationToken);
|
||||||
await RunAction(states, results, "关闭香氛灯", entities.FragranceLight, "light", "turn_off", null, cancellationToken);
|
await RunAction(states, results, "关闭香氛灯", entities.FragranceLight, "light", "turn_off", null, cancellationToken);
|
||||||
@@ -835,7 +1023,8 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
AddFallback("fallback_dehumidifier", "米家变频净化除湿机 Max", "xiaomi.derh.60l", nameof(HealthDeviceCategory.EnvironmentControl), "睡眠空间",
|
AddFallback("fallback_dehumidifier", "米家变频净化除湿机 Max", "xiaomi.derh.60l", nameof(HealthDeviceCategory.EnvironmentControl), "睡眠空间",
|
||||||
entities.Dehumidifier, entities.DehumidifierFanLevel, entities.DehumidifierUv);
|
entities.Dehumidifier, entities.DehumidifierFanLevel, entities.DehumidifierUv);
|
||||||
AddFallback("fallback_fragrance", "米家智能香氛机", "xiaomi.diffuser.xw002", nameof(HealthDeviceCategory.Fragrance), "睡眠空间",
|
AddFallback("fallback_fragrance", "米家智能香氛机", "xiaomi.diffuser.xw002", nameof(HealthDeviceCategory.Fragrance), "睡眠空间",
|
||||||
entities.FragranceSwitch, entities.FragranceLight);
|
entities.FragranceSwitch, entities.FragranceLight, entities.FragranceDuration, entities.FragranceInterval,
|
||||||
|
entities.FragranceQuickRelease, entities.FragranceAutoRelease, entities.FragranceAutoLight, entities.FragranceLightAutoOff);
|
||||||
AddFallback("fallback_ambient_light", "米家夜灯/氛围灯", "lemesh.light.wy0d02", nameof(HealthDeviceCategory.Lighting), "睡眠空间",
|
AddFallback("fallback_ambient_light", "米家夜灯/氛围灯", "lemesh.light.wy0d02", nameof(HealthDeviceCategory.Lighting), "睡眠空间",
|
||||||
entities.AmbientLight);
|
entities.AmbientLight);
|
||||||
AddFallback("fallback_speaker", "Xiaomi Sound Pro 智能音箱", "xiaomi.wifispeaker.l17a", nameof(HealthDeviceCategory.Media), "睡眠空间",
|
AddFallback("fallback_speaker", "Xiaomi Sound Pro 智能音箱", "xiaomi.wifispeaker.l17a", nameof(HealthDeviceCategory.Media), "睡眠空间",
|
||||||
@@ -1069,6 +1258,13 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
["color_temp"] = _options.Entities.AmbientLight,
|
["color_temp"] = _options.Entities.AmbientLight,
|
||||||
["volume"] = _options.Entities.Speaker,
|
["volume"] = _options.Entities.Speaker,
|
||||||
["speaker_sleep_mode"] = _options.Entities.SpeakerSleepMode,
|
["speaker_sleep_mode"] = _options.Entities.SpeakerSleepMode,
|
||||||
|
["fragrance_duration"] = _options.Entities.FragranceDuration,
|
||||||
|
["fragrance_interval"] = _options.Entities.FragranceInterval,
|
||||||
|
["fragrance_light"] = _options.Entities.FragranceLight,
|
||||||
|
["fragrance_quick_release"] = _options.Entities.FragranceQuickRelease,
|
||||||
|
["fragrance_auto_release"] = _options.Entities.FragranceAutoRelease,
|
||||||
|
["fragrance_auto_light"] = _options.Entities.FragranceAutoLight,
|
||||||
|
["fragrance_light_auto_off"] = _options.Entities.FragranceLightAutoOff,
|
||||||
["mattress_left_heating"] = _options.Entities.MattressLeftHeatingLevel,
|
["mattress_left_heating"] = _options.Entities.MattressLeftHeatingLevel,
|
||||||
["mattress_right_heating"] = _options.Entities.MattressRightHeatingLevel,
|
["mattress_right_heating"] = _options.Entities.MattressRightHeatingLevel,
|
||||||
["mattress_left_mode"] = _options.Entities.MattressLeftMode,
|
["mattress_left_mode"] = _options.Entities.MattressLeftMode,
|
||||||
@@ -1132,7 +1328,6 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
var dehumMode = userConfig?.DehumidifierMode ?? defaultScene.DehumidifierMode;
|
var dehumMode = userConfig?.DehumidifierMode ?? defaultScene.DehumidifierMode;
|
||||||
var fanLevel = userConfig?.FanLevel ?? defaultScene.FanLevel;
|
var fanLevel = userConfig?.FanLevel ?? defaultScene.FanLevel;
|
||||||
var enableUv = userConfig?.EnableUv ?? defaultScene.EnableUv;
|
var enableUv = userConfig?.EnableUv ?? defaultScene.EnableUv;
|
||||||
var lightBrightness = userConfig?.LightBrightnessPercent ?? defaultScene.LightBrightnessPercent;
|
|
||||||
var lightColorTemp = userConfig?.LightColorTemperatureKelvin ?? defaultScene.LightColorTemperatureKelvin;
|
var lightColorTemp = userConfig?.LightColorTemperatureKelvin ?? defaultScene.LightColorTemperatureKelvin;
|
||||||
var enableFragrance = userConfig?.EnableFragrance ?? defaultScene.EnableFragrance;
|
var enableFragrance = userConfig?.EnableFragrance ?? defaultScene.EnableFragrance;
|
||||||
var enableSpeaker = userConfig?.EnableSpeakerSleepMode ?? defaultScene.EnableSpeakerSleepMode;
|
var enableSpeaker = userConfig?.EnableSpeakerSleepMode ?? defaultScene.EnableSpeakerSleepMode;
|
||||||
@@ -1177,11 +1372,11 @@ public sealed class HealthHomeAssistantService : IDynamicApiController, ITransie
|
|||||||
DeviceName = "Lemesh 色温灯",
|
DeviceName = "Lemesh 色温灯",
|
||||||
Category = nameof(HealthDeviceCategory.Lighting),
|
Category = nameof(HealthDeviceCategory.Lighting),
|
||||||
EntityId = _options.Entities.AmbientLight,
|
EntityId = _options.Entities.AmbientLight,
|
||||||
Action = "开启暖光助眠",
|
Action = "开启助眠渐暗灯光",
|
||||||
Detail = $"亮度 {lightBrightness}% · 色温 {lightColorTemp}K",
|
Detail = $"从50%开始,每5秒降低1%,最终关灯 · 色温 {lightColorTemp}K",
|
||||||
Parameters = new()
|
Parameters = new()
|
||||||
{
|
{
|
||||||
["brightness"] = $"{lightBrightness}%",
|
["brightness"] = "50%→关闭",
|
||||||
["color_temp"] = $"{lightColorTemp}K"
|
["color_temp"] = $"{lightColorTemp}K"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user