This commit is contained in:
fuenmao
2026-11-12 15:45:52 +08:00
parent 61e25175db
commit 718b138033
30 changed files with 1511 additions and 99 deletions
@@ -0,0 +1,27 @@
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
using Evergreen.Health.Application.Health.Sleep;
using Microsoft.Extensions.Options;
namespace Evergreen.Health.Application.Health.Devices.Dehumidifier;
public sealed class BedPresenceDehumidifierPreview(IHomeAssistantGateway gateway, IOptions<HomeAssistantOptions> options,
TimeProvider clock) : IBedPresenceAction
{
private HomeAssistantOptions Configuration => options.Value;
private int Threshold => Configuration.SleepScene.TargetHumidity;
public string Key => "dehumidifier-humidity-preview";
public string Description => "湿度偏高时开启除湿机";
public bool PreviewOnly => true;
public string ConditionLabel => $"室内相对湿度超过 {Threshold}% 时记录开机计划。";
public bool SupportsTarget(string entityId) => DehumidifierHumidityCondition.SupportsTarget(Configuration, entityId);
public async Task<string> ExecuteAsync(string entityId, CancellationToken token)
{
if (!SupportsTarget(entityId)) return "target-not-configured";
var states = await gateway.GetStatesAsync(token);
var reason = DehumidifierHumidityCondition.SkipReason(states, Configuration, clock.GetUtcNow());
return reason is null ? "preview-would-start" : $"preview-{reason}";
}
}
@@ -0,0 +1,56 @@
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
using Evergreen.Health.Application.Health.Scenes;
using Evergreen.Health.Application.Health.Sleep;
using Microsoft.Extensions.Options;
namespace Evergreen.Health.Application.Health.Devices.Dehumidifier;
public sealed class BedPresenceDehumidifierStartAction(DehumidifierModule dehumidifier, DeviceActionRunner runner,
IOptions<HomeAssistantOptions> options, TimeProvider clock) : IBedPresenceAction
{
private HomeAssistantOptions Configuration => options.Value;
public string Key => "dehumidifier-humidity-start";
public string Description => "湿度偏高时开启除湿机";
public string ConditionLabel => $"室内相对湿度超过 {Configuration.SleepScene.TargetHumidity}% 且除湿机关机时,发送开机指令。";
public bool SupportsTarget(string entityId) => entityId == dehumidifier.PrimaryEntityId &&
DehumidifierHumidityCondition.SupportsTarget(Configuration, entityId);
public async Task<string> ExecuteAsync(string entityId, CancellationToken token)
{
if (!SupportsTarget(entityId)) return "target-not-configured";
var command = dehumidifier.BuildStartCommand() with
{
ExpectedState = "on",
RetryOnVerificationMismatch = false
};
var reason = await runner.SendIfAsync(command, states => GetSkipReason(states, entityId), token);
if (reason is not null) return reason;
try
{
await runner.VerifyParameterAsync(command, token);
return "readback-confirmed";
}
catch (InvalidOperationException)
{
return "sent-readback-unconfirmed";
}
}
private string? GetSkipReason(IReadOnlyList<HomeAssistantState> states, string entityId)
{
var reason = DehumidifierHumidityCondition.SkipReason(states, Configuration, clock.GetUtcNow());
if (reason is not null) return reason;
var state = states.FirstOrDefault(item => string.Equals(item.EntityId, entityId, StringComparison.OrdinalIgnoreCase));
if (state is null || !DeviceActionRunner.IsAvailable(state)) return "dehumidifier-unavailable";
return state.State switch
{
"off" => null,
"on" => "dehumidifier-already-on",
_ => "dehumidifier-state-not-off"
};
}
}
@@ -0,0 +1,34 @@
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
using Evergreen.Health.Application.Health.Scenes;
using System.Globalization;
namespace Evergreen.Health.Application.Health.Devices.Dehumidifier;
internal static class DehumidifierHumidityCondition
{
private static readonly TimeSpan MaxHumidityAge = TimeSpan.FromMinutes(30);
public static bool SupportsTarget(HomeAssistantOptions configuration, string entityId) =>
configuration.Enabled && configuration.SleepScene.TargetHumidity is >= 40 and <= 70 &&
entityId?.StartsWith("humidifier.", StringComparison.Ordinal) == true &&
entityId == configuration.Entities.Dehumidifier &&
configuration.Entities.Humidity?.StartsWith("sensor.", StringComparison.Ordinal) == true;
public static string? SkipReason(IReadOnlyList<HomeAssistantState> states, HomeAssistantOptions configuration, DateTimeOffset now)
{
var humidity = states.FirstOrDefault(state =>
string.Equals(state.EntityId, configuration.Entities.Humidity, StringComparison.OrdinalIgnoreCase));
if (humidity is null || !DeviceActionRunner.IsAvailable(humidity)) return "humidity-unavailable";
var observedAt = humidity.LastReported ?? humidity.LastUpdated;
if (observedAt is null || now - observedAt > MaxHumidityAge || observedAt > now.AddMinutes(1))
return "humidity-stale";
if (humidity.GetStringAttribute("unit_of_measurement") != "%" ||
!double.TryParse(humidity.State, NumberStyles.Float, CultureInfo.InvariantCulture, out var percent) ||
!double.IsFinite(percent) || percent is < 0 or > 100)
return "humidity-invalid";
return percent > configuration.SleepScene.TargetHumidity ? null : "humidity-normal";
}
}
@@ -31,10 +31,12 @@ public sealed class DehumidifierModule : HealthDeviceModule
public override string PrimaryEntityId => _entities.Dehumidifier;
public override IReadOnlyList<DeviceParameterDefinition> Parameters { get; }
internal DeviceCommand BuildStartCommand() => new("开启除湿机", PrimaryEntityId, "humidifier", "turn_on");
public override async Task ExecuteSleepAsync(DeviceSceneContext context, PersonalizedSceneConfig config,
string constitutionCode, CancellationToken cancellationToken)
{
await context.RunAsync(new("开启除湿机", PrimaryEntityId, "humidifier", "turn_on"), cancellationToken);
await context.RunAsync(BuildStartCommand(), cancellationToken);
await context.RunAsync(BuildParameterCommand("humidity", config.TargetHumidity), cancellationToken);
await context.RunAsync(BuildParameterCommand("dehumidifier_mode", config.DehumidifierMode), cancellationToken);
await context.RunAsync(BuildParameterCommand("fan_level", config.FanLevel), cancellationToken);