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,31 @@
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
using Evergreen.Health.Application.Health.Integrations.MiAirX;
using Evergreen.Health.Application.Health.Scenes;
using Evergreen.Health.Application.Health.Sleep;
using Microsoft.Extensions.Options;
namespace Evergreen.Health.Application.Health.Devices.Audio;
public sealed class BedPresenceSleepMusicAction(IHomeAssistantGateway gateway, TcmMusicTransitionService music,
IOptions<HomeAssistantOptions> options) : IBedPresenceAction
{
private const string Playlist = "balanced";
public string Key => "speaker-sleep-music";
public string Description => "用 Xiaomi Sound Pro 播放舒睡音乐";
public string ConditionLabel => "音箱空闲时播放默认舒睡曲目并渐弱音量。";
public bool SupportsTarget(string entityId) => options.Value.Enabled &&
entityId?.StartsWith("media_player.", StringComparison.Ordinal) == true && entityId == options.Value.Entities.Speaker;
public async Task<string> ExecuteAsync(string entityId, CancellationToken token)
{
if (!SupportsTarget(entityId)) return "target-not-configured";
if (!music.HasPlayableTracks(Playlist)) return "speaker-music-not-configured";
var states = await gateway.GetStatesAsync(token);
var speaker = states.FirstOrDefault(state => string.Equals(state.EntityId, entityId, StringComparison.OrdinalIgnoreCase));
if (speaker is null || !DeviceActionRunner.IsAvailable(speaker)) return "speaker-state-unavailable";
if (speaker.State is not ("idle" or "off" or "standby")) return "speaker-not-idle";
return await music.TryStartIdlePlaybackAsync(Playlist, token) ? "speaker-playback-sent" : "speaker-not-idle";
}
}
@@ -0,0 +1,82 @@
using Evergreen.Health.Application.Health.Devices.Contracts;
using Evergreen.Health.Application.Health.Devices.Environment;
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
using Evergreen.Health.Application.Health.Policies;
using Evergreen.Health.Application.Health.Scenes;
using Evergreen.Health.Application.Health.Sleep;
using Microsoft.Extensions.Options;
namespace Evergreen.Health.Application.Health.Devices.Climate;
public sealed class BedPresenceClimateHeatAction(ClimateModule climate, IndoorEnvironmentReader environment,
DeviceActionRunner runner, IOptions<HomeAssistantOptions> options) : IBedPresenceAction
{
private const int TargetTemperature = 22;
public string Key => "climate-heat-start";
public string Description => "室温偏低时开启空调制热";
public string ConditionLabel => $"室温低于 {TargetTemperature}℃ 且空调关机时,开启制热并设为 {TargetTemperature}℃。";
public TimeSpan ExecutionTimeout => TimeSpan.FromSeconds(40);
public bool SupportsTarget(string entityId) => options.Value.Enabled &&
entityId?.StartsWith("climate.", StringComparison.Ordinal) == true && entityId == climate.PrimaryEntityId &&
options.Value.Entities.Temperature?.StartsWith("sensor.", StringComparison.Ordinal) == true;
public async Task<string> ExecuteAsync(string entityId, CancellationToken token)
{
if (!SupportsTarget(entityId)) return "target-not-configured";
var mode = climate.BuildParameterCommand("climate_mode", "heat");
var temperature = climate.BuildParameterCommand("climate_temperature", TargetTemperature) with
{
ExpectedEntityState = "heat"
};
var reason = await runner.SendIfAsync(mode, states => GetStartSkipReason(states, mode, temperature), token);
if (reason is not null) return reason;
try { await runner.VerifyParameterAsync(mode, token); }
catch (InvalidOperationException) { return "climate-heat-mode-unconfirmed"; }
try
{
reason = await runner.SendIfAsync(temperature, states =>
CurrentState(states, entityId)?.State == "heat" ? null : "climate-heat-state-changed", token);
if (reason is not null) return reason;
await runner.VerifyParameterAsync(temperature, token);
return "readback-confirmed";
}
catch (Exception error) when (error is not OperationCanceledException)
{
return "climate-heat-target-unconfirmed";
}
}
private string? GetStartSkipReason(IReadOnlyList<HomeAssistantState> states, DeviceCommand mode, DeviceCommand temperature)
{
var sample = environment.Read(states);
if (sample.Celsius is not { } celsius)
return sample.Issue switch
{
IndoorTemperatureIssue.Stale => "temperature-stale",
IndoorTemperatureIssue.Invalid => "temperature-invalid",
_ => "temperature-unavailable"
};
if (celsius >= TargetTemperature) return "temperature-not-low";
var state = CurrentState(states, mode.EntityId);
if (state is null || !DeviceActionRunner.IsAvailable(state)) return "climate-unavailable";
if (state.State != "off") return "climate-not-off";
// 启动前同时校验制热模式和目标温度。
try
{
mode.ValidateCurrentState!(state);
temperature.ValidateCurrentState!(state);
return null;
}
catch (InvalidOperationException) { return "climate-heat-unsupported"; }
}
private static HomeAssistantState? CurrentState(IReadOnlyList<HomeAssistantState> states, string entityId) =>
states.FirstOrDefault(state => string.Equals(state.EntityId, entityId, StringComparison.OrdinalIgnoreCase));
}
@@ -0,0 +1,47 @@
using Evergreen.Health.Application.Health.Devices.Environment;
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
using Evergreen.Health.Application.Health.Policies;
using Evergreen.Health.Application.Health.Sleep;
using Microsoft.Extensions.Options;
namespace Evergreen.Health.Application.Health.Devices.Climate;
public sealed class BedPresenceClimatePreview(IHomeAssistantGateway gateway, IndoorEnvironmentReader environment,
IOptions<HomeAssistantOptions> options) : IBedPresenceAction
{
private HomeAssistantOptions Configuration => options.Value;
public string Key => "climate-temperature-preview";
public string Description => "按室温判断空调模式";
public bool PreviewOnly => true;
public string ConditionLabel => $"室温高于 {RecommendedRoomRange.Default.Maximum}℃ 时计划制冷,低于 {RecommendedRoomRange.Default.Minimum}℃ 时计划制热。";
public bool SupportsTarget(string entityId) => Configuration.Enabled &&
entityId?.StartsWith("climate.", StringComparison.Ordinal) == true && entityId == Configuration.Entities.Climate &&
Configuration.Entities.Temperature?.StartsWith("sensor.", StringComparison.Ordinal) == true;
public async Task<string> ExecuteAsync(string entityId, CancellationToken token)
{
if (!SupportsTarget(entityId)) return "target-not-configured";
IReadOnlyList<HomeAssistantState> states;
try { states = await gateway.GetStatesAsync(token); }
catch (Exception error) when (error is not OperationCanceledException)
{
return "preview-temperature-unavailable";
}
var sample = environment.Read(states);
return RoomTemperaturePolicy.Evaluate(sample, RecommendedRoomRange.Default).Mode switch
{
"cool" => "preview-climate-cool",
"heat" => "preview-climate-heat",
"off" => "preview-temperature-within-range",
_ => sample.Issue switch
{
IndoorTemperatureIssue.Stale => "preview-temperature-stale",
IndoorTemperatureIssue.Invalid => "preview-temperature-invalid",
_ => "preview-temperature-unavailable"
}
};
}
}
@@ -13,6 +13,7 @@ public sealed record DeviceCommand(string Name, string EntityId, string Domain,
public Action<HomeAssistantState>? ValidateCurrentState { get; init; }
// 不为空时 ExpectedState 表示此属性的期望值,而不是实体的顶层状态。
public string? ExpectedAttribute { get; init; }
public string? ExpectedEntityState { get; init; }
public bool RetryOnVerificationMismatch { get; init; } = true;
public Dictionary<string, object?> ToServiceData()
{
@@ -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);
@@ -13,22 +13,30 @@ public sealed class IndoorEnvironmentReader
public IndoorEnvironmentReader(IOptions<HomeAssistantOptions> options, TimeProvider time)
=> (_entities, _time) = (options.Value.Entities, time);
public IndoorTemperatureSample Read(IReadOnlyDictionary<string, HomeAssistantState> states)
public IndoorTemperatureSample Read(IReadOnlyDictionary<string, HomeAssistantState> states) =>
ReadState(string.IsNullOrWhiteSpace(_entities.Temperature) ? null : states.GetValueOrDefault(_entities.Temperature));
public IndoorTemperatureSample Read(IReadOnlyList<HomeAssistantState> states) =>
ReadState(states.LastOrDefault(state =>
string.Equals(state.EntityId, _entities.Temperature, StringComparison.OrdinalIgnoreCase)));
private IndoorTemperatureSample ReadState(HomeAssistantState? state)
{
if (string.IsNullOrWhiteSpace(_entities.Temperature) ||
!states.TryGetValue(_entities.Temperature, out var state) || !DeviceActionRunner.IsAvailable(state))
return new(null, null, false, "室内温度实体未配置或不可用");
if (state is null || !DeviceActionRunner.IsAvailable(state))
return new(null, null, false, "室内温度实体未配置或不可用") { Issue = IndoorTemperatureIssue.Unavailable };
var observedAt = state.LastReported ?? state.LastUpdated;
if (observedAt is null || _time.GetUtcNow() - observedAt > TimeSpan.FromMinutes(30) ||
observedAt > _time.GetUtcNow().AddMinutes(1))
return new(null, observedAt, false, "室内温度数据缺少有效时间或已过期");
var now = _time.GetUtcNow();
if (observedAt is null || now - observedAt > TimeSpan.FromMinutes(30) || observedAt > now.AddMinutes(1))
return new(null, observedAt, false, "室内温度数据缺少有效时间或已过期") { Issue = IndoorTemperatureIssue.Stale };
if (!double.TryParse(state.State, NumberStyles.Float, CultureInfo.InvariantCulture, out var celsius) ||
!double.IsFinite(celsius)) return new(null, observedAt, false, "室内温度数据无效");
!double.IsFinite(celsius))
return new(null, observedAt, false, "室内温度数据无效") { Issue = IndoorTemperatureIssue.Invalid };
var unit = state.GetStringAttribute("unit_of_measurement");
if (unit is "°F" or "F") celsius = (celsius - 32) * 5 / 9;
else if (!string.IsNullOrWhiteSpace(unit) && unit is not ("°C" or "C" or "℃"))
return new(null, observedAt, false, "室内温度单位未识别");
if (celsius is < -20 or > 60) return new(null, observedAt, false, "室内温度超出有效范围");
return new(null, observedAt, false, "室内温度单位未识别") { Issue = IndoorTemperatureIssue.Invalid };
if (celsius is < -20 or > 60)
return new(null, observedAt, false, "室内温度超出有效范围") { Issue = IndoorTemperatureIssue.Invalid };
return new(celsius, observedAt, true, "室内温度数据有效");
}
}
@@ -0,0 +1,27 @@
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.Fragrance;
public sealed class BedPresenceFragranceReleaseAction(DeviceActionRunner runner, IOptions<HomeAssistantOptions> options,
bool wakeDevice) : IBedPresenceAction
{
private string ConfiguredButton => wakeDevice ? options.Value.Entities.WakeFragranceQuickRelease : options.Value.Entities.FragranceQuickRelease;
public string Key => wakeDevice ? "wake-fragrance-quick-release" : "fragrance-quick-release";
public string Description => wakeDevice ? "让香氛机「唤醒」出香一次" : "让米家智能香氛机出香一次";
public bool SupportsTarget(string entityId) => options.Value.Enabled &&
entityId?.StartsWith("button.", StringComparison.Ordinal) == true && entityId == ConfiguredButton;
public async Task<string> ExecuteAsync(string entityId, CancellationToken token)
{
if (!SupportsTarget(entityId)) return "target-not-configured";
var reason = await runner.SendIfAsync(FragranceModule.BuildQuickRelease(entityId), states =>
states.Any(state => string.Equals(state.EntityId, entityId, StringComparison.OrdinalIgnoreCase) &&
DeviceActionRunner.IsAvailable(state)) ? null : "fragrance-release-unavailable", token);
return reason ?? "fragrance-release-sent";
}
}
@@ -9,36 +9,52 @@ namespace Evergreen.Health.Application.Health.Devices.Fragrance;
public sealed class FragranceModule : HealthDeviceModule
{
private readonly HomeAssistantEntityOptions _entities;
public FragranceModule(DeviceActionRunner runner, IOptions<HomeAssistantOptions> options) : base(runner)
private readonly bool _wakeDevice;
public FragranceModule(DeviceActionRunner runner, IOptions<HomeAssistantOptions> options)
: this(runner, options, wakeDevice: false) { }
internal FragranceModule(DeviceActionRunner runner, IOptions<HomeAssistantOptions> options, bool wakeDevice) : base(runner)
{
_entities = options.Value.Entities;
Parameters =
_wakeDevice = wakeDevice;
var duration = wakeDevice ? _entities.WakeFragranceDuration : _entities.FragranceDuration;
var intervalEntity = wakeDevice ? _entities.WakeFragranceInterval : _entities.FragranceInterval;
var light = wakeDevice ? _entities.WakeFragranceLight : _entities.FragranceLight;
var quickRelease = wakeDevice ? _entities.WakeFragranceQuickRelease : _entities.FragranceQuickRelease;
var autoRelease = wakeDevice ? _entities.WakeFragranceAutoRelease : _entities.FragranceAutoRelease;
var autoLight = wakeDevice ? _entities.WakeFragranceAutoLight : _entities.FragranceAutoLight;
var lightAutoOff = wakeDevice ? _entities.WakeFragranceLightAutoOff : _entities.FragranceLightAutoOff;
DeviceParameterDefinition[] parameters =
[
DeviceValues.Number("fragrance_duration", _entities.FragranceDuration, 2, 6, "出香时长"),
new("fragrance_interval", _entities.FragranceInterval, value =>
DeviceValues.Number("fragrance_duration", duration, 2, 6, "出香时长"),
new("fragrance_interval", intervalEntity, value =>
{
var interval = DeviceValues.Integer(value, 10, 20, "出香间隔");
if (interval % 5 != 0) throw new InvalidOperationException("出香间隔仅支持 10、15 或 20 分钟");
return new("设置出香间隔", _entities.FragranceInterval, "number", "set_value",
return new("设置出香间隔", intervalEntity, "number", "set_value",
new Dictionary<string, object?> { ["value"] = interval });
}, "出香间隔", 10, 20),
DeviceValues.Switch("fragrance_light", _entities.FragranceLight, "light", "香氛灯"),
new("fragrance_quick_release", _entities.FragranceQuickRelease, _ => new("快速出香",
_entities.FragranceQuickRelease, "button", "press"), "快速出香"),
DeviceValues.Switch("fragrance_auto_release", _entities.FragranceAutoRelease, "switch", "人来增香"),
DeviceValues.Switch("fragrance_auto_light", _entities.FragranceAutoLight, "switch", "感应夜灯"),
DeviceValues.Switch("fragrance_light_auto_off", _entities.FragranceLightAutoOff, "switch", "夜灯自动关闭")
DeviceValues.Switch("fragrance_light", light, "light", "香氛灯"),
new("fragrance_quick_release", quickRelease, _ => BuildQuickRelease(quickRelease), "快速出香"),
DeviceValues.Switch("fragrance_auto_release", autoRelease, "switch", "人来增香"),
DeviceValues.Switch("fragrance_auto_light", autoLight, "switch", "感应夜灯"),
DeviceValues.Switch("fragrance_light_auto_off", lightAutoOff, "switch", "夜灯自动关闭")
];
Parameters = wakeDevice ? parameters.Select(parameter => parameter with { Key = $"wake_{parameter.Key}" }).ToArray() : parameters;
}
public override string Key => "fragrance";
public override string Key => _wakeDevice ? "wake-fragrance" : "fragrance";
public override string Category => nameof(HealthDeviceCategory.Fragrance);
public override string DisplayName => "智能香氛机";
public override string PrimaryEntityId => _entities.FragranceSwitch;
public override string DisplayName => _wakeDevice ? "香氛机 唤醒" : "智能香氛机";
public override string PrimaryEntityId => _wakeDevice ? _entities.WakeFragranceSwitch : _entities.FragranceSwitch;
public override IReadOnlyList<DeviceParameterDefinition> Parameters { get; }
internal static DeviceCommand BuildQuickRelease(string entityId) => new("快速出香", entityId, "button", "press");
public override async Task ExecuteSleepAsync(DeviceSceneContext context, PersonalizedSceneConfig config,
string constitutionCode, CancellationToken cancellationToken)
{
if (_wakeDevice) return;
await context.RunAsync(new(config.EnableFragrance ? "开启香氛机" : "关闭香氛机",
PrimaryEntityId, "switch", config.EnableFragrance ? "turn_on" : "turn_off"), cancellationToken);
if (config.EnableFragrance)
@@ -53,7 +69,7 @@ public sealed class FragranceModule : HealthDeviceModule
cancellationToken);
}
public override IEnumerable<DeviceCommand> GetShutdownCommands() =>
public override IEnumerable<DeviceCommand> GetShutdownCommands() => _wakeDevice ? [] :
[
new("关闭香氛机", PrimaryEntityId, "switch", "turn_off", ExpectedState: "off"),
new("关闭香氛灯", _entities.FragranceLight, "light", "turn_off", ExpectedState: "off"),
@@ -62,7 +78,7 @@ public sealed class FragranceModule : HealthDeviceModule
];
public override HomeAssistantScenePresetItem? BuildPreview(PersonalizedSceneConfig config, string constitutionCode)
=> string.IsNullOrWhiteSpace(PrimaryEntityId) ? null : Preview(config.EnableFragrance ? "开启香氛释放" : "关闭香氛释放",
=> _wakeDevice || string.IsNullOrWhiteSpace(PrimaryEntityId) ? null : Preview(config.EnableFragrance ? "开启香氛释放" : "关闭香氛释放",
config.EnableFragrance ? "开启香氛机;香氛灯可用时同步开启柔光" : "当前体质预设不启动香氛",
new() { ["power"] = config.EnableFragrance ? "开启" : "关闭" });
}
@@ -34,6 +34,16 @@ public static class HealthDeviceRegistration
services.AddOptions<BedPresenceAutomationOptions>();
services.AddSingleton<IBedPresenceAction, BedPresenceSleepLightAction>();
services.AddSingleton<IBedPresenceAction, BedPresenceCurtainCloseAction>();
services.AddSingleton<IBedPresenceAction, BedPresenceDehumidifierPreview>();
services.AddSingleton<IBedPresenceAction, BedPresenceDehumidifierStartAction>();
services.AddSingleton<IBedPresenceAction, BedPresenceClimatePreview>();
services.AddSingleton<IBedPresenceAction, BedPresenceClimateHeatAction>();
services.AddSingleton<IBedPresenceAction>(provider => new BedPresenceFragranceReleaseAction(
provider.GetRequiredService<DeviceActionRunner>(), provider.GetRequiredService<IOptions<HomeAssistantOptions>>(), wakeDevice: false));
services.AddSingleton<IBedPresenceAction>(provider => new BedPresenceFragranceReleaseAction(
provider.GetRequiredService<DeviceActionRunner>(), provider.GetRequiredService<IOptions<HomeAssistantOptions>>(), wakeDevice: true));
services.AddSingleton<IBedPresenceAction, BedPresenceSleepMusicAction>();
services.AddSingleton<IBedPresenceExecutionLog, Ib43AutomationExecutionLog>();
services.AddSingleton<BedPresenceAutomation>();
services.AddSingleton<IBedPresenceSink>(provider => provider.GetRequiredService<BedPresenceAutomation>());
services.AddHostedService(provider => provider.GetRequiredService<BedPresenceAutomation>());
@@ -74,6 +84,8 @@ public static class HealthDeviceRegistration
services.AddSingleton<IHealthDeviceModule>(provider => provider.GetRequiredService<DehumidifierModule>());
services.AddSingleton<FragranceModule>();
services.AddSingleton<IHealthDeviceModule>(provider => provider.GetRequiredService<FragranceModule>());
services.AddSingleton<IHealthDeviceModule>(provider => new FragranceModule(
provider.GetRequiredService<DeviceActionRunner>(), provider.GetRequiredService<IOptions<HomeAssistantOptions>>(), wakeDevice: true));
services.AddSingleton<LightingModule>(provider => new(
provider.GetRequiredService<DeviceActionRunner>(), provider.GetRequiredService<IOptions<HomeAssistantOptions>>(),
provider.GetRequiredService<HomeAssistantLightTransitionService>().ForEntity(