11
This commit is contained in:
+47
-246
@@ -6,268 +6,69 @@ using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||
|
||||
/// <summary>在服务端驱动卧室灯光渐变,浏览器关闭后任务仍继续。</summary>
|
||||
/// <summary>管理明确配置的场景灯;共享单灯渐变实现,但不共享亮度、计时或取消状态。</summary>
|
||||
public sealed class HomeAssistantLightTransitionService : IHostedService, ILightTransitionRuntime
|
||||
{
|
||||
private const int SleepStartPercent = 50;
|
||||
private const int WakeTargetPercent = 70;
|
||||
private static readonly TimeSpan StepInterval = TimeSpan.FromSeconds(5);
|
||||
private readonly LightTransitionWorker _primary;
|
||||
private readonly Dictionary<string, LightTransitionWorker> _lights = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
private readonly IHomeAssistantGateway _homeAssistant;
|
||||
private readonly LightOffDispatcher _lightOff;
|
||||
private readonly SceneLightSafetyState _safety;
|
||||
private readonly TimeProvider _time;
|
||||
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(
|
||||
IHomeAssistantGateway homeAssistant,
|
||||
LightOffDispatcher lightOff,
|
||||
IOptions<HomeAssistantOptions> options,
|
||||
ILogger<HomeAssistantLightTransitionService> logger,
|
||||
SceneLightSafetyState safety,
|
||||
TimeProvider time)
|
||||
public HomeAssistantLightTransitionService(IHomeAssistantGateway gateway, LightOffDispatcher lightOff,
|
||||
IOptions<HomeAssistantOptions> options, ILogger<HomeAssistantLightTransitionService> logger,
|
||||
SceneLightSafetyState safety, TimeProvider time)
|
||||
{
|
||||
_options = options.Value;
|
||||
_logger = logger;
|
||||
_homeAssistant = homeAssistant;
|
||||
_lightOff = lightOff;
|
||||
_safety = safety;
|
||||
_time = time;
|
||||
_primary = new(gateway, lightOff, options, logger, safety, time);
|
||||
var primaryId = options.Value.Entities.AmbientLight;
|
||||
if (!string.IsNullOrWhiteSpace(primaryId)) _lights.Add(primaryId, _primary);
|
||||
foreach (var light in options.Value.AdditionalSleepLights)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(light.Key) || string.IsNullOrWhiteSpace(light.Name) ||
|
||||
!light.EntityId.StartsWith("light.", StringComparison.Ordinal))
|
||||
throw new InvalidOperationException("额外场景灯需要有效的模块键、名称和 light 实体");
|
||||
if (!_lights.TryAdd(light.EntityId, new(gateway, lightOff, options, logger, safety, time, light)))
|
||||
throw new InvalidOperationException($"场景灯实体重复配置:{light.EntityId}");
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _operationLock.WaitAsync(cancellationToken);
|
||||
try { await StopCurrentAsync(); }
|
||||
finally { _operationLock.Release(); }
|
||||
}
|
||||
public ILightTransitionRuntime ForEntity(string entityId)
|
||||
=> _lights.TryGetValue(entityId, out var light) ? light
|
||||
: entityId == _primary.GetStatus().EntityId ? _primary
|
||||
: throw new InvalidOperationException("该灯未配置为场景灯");
|
||||
|
||||
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
|
||||
};
|
||||
}
|
||||
var output = _primary.GetStatus();
|
||||
output.Lights = _lights.Values.Select(light => light.GetStatus()).ToList();
|
||||
return output;
|
||||
}
|
||||
|
||||
public async Task<HomeAssistantLightTransitionOutput> StartSleepAsync(int colorTemperatureKelvin, CancellationToken cancellationToken)
|
||||
// 旧助眠/晨起接口只作用于原卧室灯;额外灯由各自场景模块调用。
|
||||
public async Task<HomeAssistantLightTransitionOutput> StartSleepAsync(int kelvin, CancellationToken token)
|
||||
{
|
||||
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);
|
||||
|
||||
_safety.MarkOn();
|
||||
SetStatus(true, true, "sleep", SleepStartPercent, 0, "助眠渐暗已启动,每5秒降低1%亮度");
|
||||
Launch("sleep", SleepStartPercent);
|
||||
return GetStatus();
|
||||
}
|
||||
finally { _operationLock.Release(); }
|
||||
await _primary.StartSleepAsync(kelvin, token);
|
||||
return GetStatus();
|
||||
}
|
||||
public async Task<HomeAssistantLightTransitionOutput> StartWakeAsync(CancellationToken token)
|
||||
{
|
||||
await _primary.StartWakeAsync(token);
|
||||
return GetStatus();
|
||||
}
|
||||
public async Task<HomeAssistantLightTransitionOutput> CancelPrimaryAsync(CancellationToken token)
|
||||
{
|
||||
await _primary.CancelAsync(token);
|
||||
return GetStatus();
|
||||
}
|
||||
|
||||
public async Task<HomeAssistantLightTransitionOutput> StartWakeAsync(CancellationToken cancellationToken)
|
||||
// 一键关闭/场景替换必须取消全部灯的计时任务,防止后续 tick 重新点亮灯。
|
||||
public async Task<HomeAssistantLightTransitionOutput> CancelAsync(CancellationToken token)
|
||||
{
|
||||
await _operationLock.WaitAsync(cancellationToken);
|
||||
try
|
||||
{
|
||||
await StopCurrentAsync();
|
||||
var light = await ReadAvailableLightAsync(cancellationToken);
|
||||
var knownOffAt = _safety.OffAt;
|
||||
var isOff = string.Equals(light.State, "off", StringComparison.OrdinalIgnoreCase) ||
|
||||
(knownOffAt is not null && (light.LastUpdated is null || light.LastUpdated <= knownOffAt));
|
||||
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)
|
||||
{
|
||||
_safety.MarkOn();
|
||||
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);
|
||||
}
|
||||
|
||||
_safety.MarkOn();
|
||||
SetStatus(true, true, "wake", current, WakeTargetPercent, "晨起唤醒已启动,每5秒增加1%亮度");
|
||||
Launch("wake", current);
|
||||
return GetStatus();
|
||||
}
|
||||
finally { _operationLock.Release(); }
|
||||
foreach (var light in _lights.Values) await light.CancelAsync(token);
|
||||
if (_lights.Count == 0) await _primary.CancelAsync(token);
|
||||
return GetStatus();
|
||||
}
|
||||
|
||||
public async Task<HomeAssistantLightTransitionOutput> CancelAsync(CancellationToken cancellationToken)
|
||||
public Task StartAsync(CancellationToken token) => Task.CompletedTask;
|
||||
public async Task StopAsync(CancellationToken token)
|
||||
{
|
||||
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, _time, cancellationToken);
|
||||
var light = await ReadAvailableLightAsync(cancellationToken);
|
||||
if (!string.Equals(light.State, "on", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_safety.MarkOff(_time.GetUtcNow());
|
||||
if (mode == "sleep")
|
||||
{
|
||||
try
|
||||
{
|
||||
await _lightOff.NotifyAsync(cancellationToken);
|
||||
SetStatus(true, false, mode, 0, 0, "灯已关闭,床垫按摩已停止");
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning(exception, "灯已关闭,但床垫按摩停止失败");
|
||||
SetStatus(false, false, mode, 0, 0, exception.Message);
|
||||
}
|
||||
}
|
||||
else SetStatus(false, false, mode, 0, WakeTargetPercent, "灯已从其他位置关闭,渐变已停止");
|
||||
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);
|
||||
_safety.MarkOff(_time.GetUtcNow());
|
||||
SetStatus(true, false, mode, 0, 0, "灯光已关闭,正在停止床垫按摩");
|
||||
try
|
||||
{
|
||||
await _lightOff.NotifyAsync(cancellationToken);
|
||||
SetStatus(true, false, mode, 0, 0, "助眠渐暗已完成,灯光与床垫按摩已关闭");
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning(exception, "助眠渐暗已完成,但床垫按摩停止失败");
|
||||
SetStatus(false, false, mode, 0, 0, exception.Message);
|
||||
}
|
||||
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
|
||||
};
|
||||
}
|
||||
foreach (var light in _lights.Values) await light.StopAsync(token);
|
||||
if (_lights.Count == 0) await _primary.StopAsync(token);
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -10,6 +10,16 @@ public sealed class HomeAssistantOptions : IConfigurableOptions
|
||||
public int TimeoutSeconds { get; set; } = 15;
|
||||
public HomeAssistantEntityOptions Entities { get; set; } = new();
|
||||
public HomeAssistantSleepSceneOptions SleepScene { get; set; } = new();
|
||||
public List<SceneLightOptions> AdditionalSleepLights { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>明确加入舒睡/全关的额外灯具;未配置的灯仍仅提供单独控制。</summary>
|
||||
public sealed class SceneLightOptions
|
||||
{
|
||||
public string Key { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string EntityId { get; set; } = string.Empty;
|
||||
public string? SleepEffect { get; set; }
|
||||
}
|
||||
|
||||
public sealed class HomeAssistantEntityOptions
|
||||
|
||||
+313
@@ -0,0 +1,313 @@
|
||||
using Evergreen.Health.Application.Health.Models;
|
||||
using Evergreen.Health.Application.Health.Scenes;
|
||||
using Evergreen.Health.Application.Health.Devices.Lighting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||
|
||||
/// <summary>单灯渐变执行器;每盏灯的计时、取消和状态互相隔离。</summary>
|
||||
internal sealed class LightTransitionWorker : ILightTransitionRuntime
|
||||
{
|
||||
private const int SleepStartPercent = 50;
|
||||
private const int WakeTargetPercent = 70;
|
||||
private static readonly TimeSpan StepInterval = TimeSpan.FromSeconds(5);
|
||||
|
||||
private readonly IHomeAssistantGateway _homeAssistant;
|
||||
private readonly LightOffDispatcher _lightOff;
|
||||
private readonly SceneLightSafetyState _safety;
|
||||
private readonly TimeProvider _time;
|
||||
private readonly string _entityId;
|
||||
private readonly string? _sleepEffect;
|
||||
private readonly bool _controlsMassage;
|
||||
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 LightTransitionWorker(
|
||||
IHomeAssistantGateway homeAssistant,
|
||||
LightOffDispatcher lightOff,
|
||||
IOptions<HomeAssistantOptions> options,
|
||||
ILogger<HomeAssistantLightTransitionService> logger,
|
||||
SceneLightSafetyState safety,
|
||||
TimeProvider time, SceneLightOptions? light = null)
|
||||
{
|
||||
_entityId = light?.EntityId ?? options.Value.Entities.AmbientLight;
|
||||
_sleepEffect = light?.SleepEffect;
|
||||
_controlsMassage = light is null;
|
||||
_logger = logger;
|
||||
_homeAssistant = homeAssistant;
|
||||
_lightOff = lightOff;
|
||||
_safety = safety;
|
||||
_time = time;
|
||||
}
|
||||
|
||||
public async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _operationLock.WaitAsync(cancellationToken);
|
||||
try { await StopCurrentAsync(); }
|
||||
finally { _operationLock.Release(); }
|
||||
}
|
||||
|
||||
public HomeAssistantLightTransitionOutput GetStatus()
|
||||
{
|
||||
lock (_statusLock)
|
||||
{
|
||||
return new HomeAssistantLightTransitionOutput
|
||||
{
|
||||
EntityId = _entityId,
|
||||
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();
|
||||
var light = await ReadAvailableLightAsync(cancellationToken);
|
||||
var data = BuildSleepStartData(light, colorTemperatureKelvin);
|
||||
await _homeAssistant.CallServiceAsync("light", "turn_on", data, cancellationToken);
|
||||
|
||||
if (_controlsMassage) _safety.MarkOn();
|
||||
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 knownOffAt = _controlsMassage ? _safety.OffAt : null;
|
||||
var isOff = string.Equals(light.State, "off", StringComparison.OrdinalIgnoreCase) ||
|
||||
(knownOffAt is not null && (light.LastUpdated is null || light.LastUpdated <= knownOffAt));
|
||||
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)
|
||||
{
|
||||
if (_controlsMassage) _safety.MarkOn();
|
||||
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);
|
||||
}
|
||||
|
||||
if (_controlsMassage) _safety.MarkOn();
|
||||
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(_entityId)
|
||||
? _entityId
|
||||
: throw new InvalidOperationException("卧室灯光实体尚未配置");
|
||||
|
||||
private Dictionary<string, object?> BuildSleepStartData(HomeAssistantState state, int kelvin)
|
||||
{
|
||||
var data = new Dictionary<string, object?> { ["entity_id"] = LightEntityId, ["brightness_pct"] = SleepStartPercent };
|
||||
// 原绑定灯兼容旧 HA 能力回读;新场景灯必须经过同一套标准能力校验。
|
||||
JsonElement modes = default;
|
||||
var hasModes = state.Attributes.ValueKind == JsonValueKind.Object &&
|
||||
state.Attributes.TryGetProperty("supported_color_modes", out modes) && modes.ValueKind == JsonValueKind.Array;
|
||||
if (_controlsMassage && !hasModes)
|
||||
{
|
||||
data["color_temp_kelvin"] = Math.Clamp(kelvin, 2200, 5000);
|
||||
return data;
|
||||
}
|
||||
var device = new HomeAssistantDiscoveredDeviceOutput
|
||||
{
|
||||
PrimaryControlEntityId = LightEntityId,
|
||||
Entities = [new() { EntityId = LightEntityId, Domain = "light" }]
|
||||
};
|
||||
var controls = new StandardLightControl();
|
||||
controls.Build(device, state, "light_brightness", SleepStartPercent);
|
||||
if (hasModes && modes.EnumerateArray().Any(mode => mode.ValueKind == JsonValueKind.String && mode.GetString() == "color_temp"))
|
||||
{
|
||||
var min = state.GetDoubleAttribute("min_color_temp_kelvin");
|
||||
var max = state.GetDoubleAttribute("max_color_temp_kelvin");
|
||||
if (min is null || max is null || min <= 0 || max <= min || max > int.MaxValue ||
|
||||
min != Math.Truncate(min.Value) || max != Math.Truncate(max.Value))
|
||||
throw new InvalidOperationException("场景灯色温范围未确认,未发送启动指令");
|
||||
var target = Math.Clamp(kelvin, (int)min.Value, (int)max.Value);
|
||||
controls.Build(device, state, "light_color_temperature", target);
|
||||
data["color_temp_kelvin"] = target;
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(_sleepEffect))
|
||||
{
|
||||
controls.Build(device, state, "light_effect", _sleepEffect);
|
||||
data["effect"] = _sleepEffect;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
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 not ("on" or "off"))
|
||||
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, _time, cancellationToken);
|
||||
var light = await ReadAvailableLightAsync(cancellationToken);
|
||||
if (!string.Equals(light.State, "on", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_controlsMassage) _safety.MarkOff(_time.GetUtcNow());
|
||||
if (mode == "sleep" && _controlsMassage)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _lightOff.NotifyAsync(cancellationToken);
|
||||
SetStatus(true, false, mode, 0, 0, "灯已关闭,床垫按摩已停止");
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning(exception, "灯已关闭,但床垫按摩停止失败");
|
||||
SetStatus(false, false, mode, 0, 0, exception.Message);
|
||||
}
|
||||
}
|
||||
else SetStatus(true, 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);
|
||||
if (_controlsMassage) _safety.MarkOff(_time.GetUtcNow());
|
||||
SetStatus(true, false, mode, 0, 0, _controlsMassage ? "灯光已关闭,正在停止床垫按摩" : "助眠渐暗已完成,灯光已关闭");
|
||||
try
|
||||
{
|
||||
if (_controlsMassage) await _lightOff.NotifyAsync(cancellationToken);
|
||||
SetStatus(true, false, mode, 0, 0, _controlsMassage ? "助眠渐暗已完成,灯光与床垫按摩已关闭" : "助眠渐暗已完成,灯光已关闭");
|
||||
}
|
||||
catch (Exception exception) when (exception is not OperationCanceledException)
|
||||
{
|
||||
_logger.LogWarning(exception, "助眠渐暗已完成,但床垫按摩停止失败");
|
||||
SetStatus(false, false, mode, 0, 0, exception.Message);
|
||||
}
|
||||
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
|
||||
{
|
||||
EntityId = _entityId,
|
||||
Success = success,
|
||||
Running = running,
|
||||
Mode = mode,
|
||||
BrightnessPercent = brightness,
|
||||
TargetPercent = target,
|
||||
Message = message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user