111111
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
using Evergreen.Health.Application.Health.Devices.Climate;
|
||||
using Evergreen.Health.Application.Health.Devices.Curtain;
|
||||
using Evergreen.Health.Application.Health.Devices.Dehumidifier;
|
||||
using Evergreen.Health.Application.Health.Devices.Environment;
|
||||
using Evergreen.Health.Application.Health.Devices.Lighting;
|
||||
using Evergreen.Health.Application.Health.Integrations.HomeAssistant;
|
||||
using Evergreen.Health.Application.Health.Scenes;
|
||||
@@ -16,6 +19,10 @@ public sealed class BedPresenceAutomationTests
|
||||
private const string Binding = "a98788f2aeeb40b68f145a182d99f832";
|
||||
private const string Light = "light.test_bedroom";
|
||||
private const string Curtain = "cover.test_curtain";
|
||||
private const string Dehumidifier = "humidifier.test";
|
||||
private const string Humidity = "sensor.test_humidity";
|
||||
private const string Climate = "climate.test";
|
||||
private const string Temperature = "sensor.test_temperature";
|
||||
private static BedPresenceSample Sample(BedPresence presence, DateTimeOffset at, Guid connection) =>
|
||||
new(Binding, "evergreen-test", "IB43SIM001", BedSide.Left, true, presence, at, connection);
|
||||
|
||||
@@ -67,6 +74,158 @@ public sealed class BedPresenceAutomationTests
|
||||
finally { await automation.StopAsync(default); }
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Stopping_and_reopening_rule_cancels_the_old_action_waiting_for_persistence()
|
||||
{
|
||||
var time = new ManualTestTimeProvider();
|
||||
var options = Options.Create(new BedPresenceAutomationOptions
|
||||
{ Mode = BedAutomationMode.Managed, ActionKey = "curtain-close", TargetEntityId = Curtain });
|
||||
var action = new NeverExecuteAction();
|
||||
var executionLog = new PausedExecutionLog();
|
||||
using var automation = new BedPresenceAutomation(options, [action], time, NullLogger<BedPresenceAutomation>.Instance, executionLog);
|
||||
await automation.StartAsync(default);
|
||||
try
|
||||
{
|
||||
var bed = new BedAutomationBinding(Binding, 1, "evergreen-test", "IB43SIM001", BedSide.Left, true);
|
||||
var connection = Guid.NewGuid();
|
||||
Assert.True(automation.TryEnable(bed));
|
||||
automation.Observe(Sample(BedPresence.Empty, time.GetUtcNow(), connection), default);
|
||||
automation.Observe(Sample(BedPresence.Occupied, time.GetUtcNow(), connection), default);
|
||||
await executionLog.Started.Task.WaitAsync(TimeSpan.FromSeconds(3));
|
||||
automation.Disable(Binding, 1);
|
||||
Assert.True(automation.TryEnable(bed));
|
||||
executionLog.Release.TrySetResult("old-execution");
|
||||
Assert.Equal("cancelled-unconfirmed", await executionLog.Completed.Task.WaitAsync(TimeSpan.FromSeconds(3)));
|
||||
Assert.Equal(0, action.Calls);
|
||||
Assert.True(automation.IsEnabled(Binding, 1));
|
||||
}
|
||||
finally
|
||||
{
|
||||
executionLog.Release.TrySetResult(null);
|
||||
await automation.StopAsync(default);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PausedExecutionLog : IBedPresenceExecutionLog
|
||||
{
|
||||
public TaskCompletionSource Started { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public TaskCompletionSource<string?> Release { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public TaskCompletionSource<string> Completed { get; } = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
public Task<string?> BeginAsync(string bindingId, long userId, string action, string target, DateTimeOffset triggeredAt,
|
||||
CancellationToken token)
|
||||
{ Started.TrySetResult(); return Release.Task; }
|
||||
public Task CompleteAsync(string id, string result, DateTimeOffset completedAt)
|
||||
{ Completed.TrySetResult(result); return Task.CompletedTask; }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("26", "°C", 0, false, "preview-climate-cool")]
|
||||
[InlineData("21", "°C", 0, false, "preview-climate-heat")]
|
||||
[InlineData("82.4", "°F", 0, false, "preview-climate-cool")]
|
||||
[InlineData("22", "°C", 0, false, "preview-temperature-within-range")]
|
||||
[InlineData("25", "°C", 0, false, "preview-temperature-within-range")]
|
||||
[InlineData(null, "°C", 0, false, "preview-temperature-unavailable")]
|
||||
[InlineData("26", "°C", 0, true, "preview-temperature-unavailable")]
|
||||
[InlineData("26", "°C", 31, false, "preview-temperature-stale")]
|
||||
[InlineData("26", "°C", null, false, "preview-temperature-stale")]
|
||||
[InlineData("26", "%", 0, false, "preview-temperature-invalid")]
|
||||
[InlineData("NaN", "°C", 0, false, "preview-temperature-invalid")]
|
||||
public async Task Climate_preview_uses_the_default_range_and_never_sends_commands(
|
||||
string? state, string unit, int? ageMinutes, bool readFails, string expected)
|
||||
{
|
||||
var clock = new ManualTestTimeProvider();
|
||||
var gateway = new FakeHaGateway { ReadsToFail = readFails ? 1 : 0 };
|
||||
if (state is not null)
|
||||
gateway.States[Temperature] = new()
|
||||
{
|
||||
EntityId = Temperature, State = state,
|
||||
LastReported = ageMinutes is { } age ? clock.GetUtcNow().AddMinutes(-age) : null,
|
||||
Attributes = JsonSerializer.SerializeToElement(new { unit_of_measurement = unit })
|
||||
};
|
||||
var options = Options.Create(new HomeAssistantOptions { Entities = new() { Climate = Climate, Temperature = Temperature } });
|
||||
var preview = new BedPresenceClimatePreview(gateway, new IndoorEnvironmentReader(options, clock), options);
|
||||
Assert.True(preview.PreviewOnly);
|
||||
Assert.Equal("target-not-configured", await preview.ExecuteAsync("climate.other", default));
|
||||
Assert.Equal(expected, await preview.ExecuteAsync(Climate, default));
|
||||
Assert.Empty(gateway.Calls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("64", "%", 0, "preview-would-start")]
|
||||
[InlineData("55", "%", 0, "preview-humidity-normal")]
|
||||
[InlineData("45", "%", 0, "preview-humidity-normal")]
|
||||
[InlineData(null, "%", 0, "preview-humidity-unavailable")]
|
||||
[InlineData("unavailable", "%", 0, "preview-humidity-unavailable")]
|
||||
[InlineData("64", "%", 31, "preview-humidity-stale")]
|
||||
[InlineData("64", "%", -2, "preview-humidity-stale")]
|
||||
[InlineData("64", "%", null, "preview-humidity-stale")]
|
||||
[InlineData("64", "°C", 0, "preview-humidity-invalid")]
|
||||
[InlineData("NaN", "%", 0, "preview-humidity-invalid")]
|
||||
[InlineData("101", "%", 0, "preview-humidity-invalid")]
|
||||
public async Task Dehumidifier_preview_records_humidity_decision_without_sending_commands(
|
||||
string? state, string unit, int? ageMinutes, string expected)
|
||||
{
|
||||
var clock = new ManualTestTimeProvider();
|
||||
var gateway = new FakeHaGateway();
|
||||
if (state is not null)
|
||||
gateway.States[Humidity] = new()
|
||||
{
|
||||
EntityId = Humidity, State = state,
|
||||
LastUpdated = ageMinutes is { } age ? clock.GetUtcNow().AddMinutes(-age) : null,
|
||||
Attributes = JsonSerializer.SerializeToElement(new { unit_of_measurement = unit })
|
||||
};
|
||||
var options = Options.Create(new HomeAssistantOptions
|
||||
{
|
||||
Entities = new() { Dehumidifier = Dehumidifier, Humidity = Humidity },
|
||||
SleepScene = new() { TargetHumidity = 55 }
|
||||
});
|
||||
var preview = new BedPresenceDehumidifierPreview(gateway, options, clock);
|
||||
Assert.True(preview.PreviewOnly);
|
||||
Assert.Equal("target-not-configured", await preview.ExecuteAsync("humidifier.other", default));
|
||||
Assert.Equal(expected, await preview.ExecuteAsync(Dehumidifier, default));
|
||||
Assert.Empty(gateway.Calls);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("64", 0, "off", "on", "readback-confirmed", 1)]
|
||||
[InlineData("64", 0, "off", "off", "sent-readback-unconfirmed", 1)]
|
||||
[InlineData("55", 0, "off", "on", "humidity-normal", 0)]
|
||||
[InlineData("64", 31, "off", "on", "humidity-stale", 0)]
|
||||
[InlineData(null, 0, "off", "on", "humidity-unavailable", 0)]
|
||||
[InlineData("101", 0, "off", "on", "humidity-invalid", 0)]
|
||||
[InlineData("64", 0, "on", "on", "dehumidifier-already-on", 0)]
|
||||
[InlineData("64", 0, "unavailable", "on", "dehumidifier-unavailable", 0)]
|
||||
[InlineData("64", 0, "unknown-state", "on", "dehumidifier-state-not-off", 0)]
|
||||
public async Task Dehumidifier_rule_requires_fresh_high_humidity_and_off_state_and_never_resends(
|
||||
string? humidity, int ageMinutes, string power, string readback, string result, int commandCount)
|
||||
{
|
||||
var clock = new ManualTestTimeProvider();
|
||||
var gateway = new FakeHaGateway { HumidifierTurnOnResult = readback };
|
||||
gateway.States[Dehumidifier] = new() { EntityId = Dehumidifier, State = power };
|
||||
if (humidity is not null)
|
||||
gateway.States[Humidity] = new() { EntityId = Humidity, State = humidity,
|
||||
LastReported = clock.GetUtcNow().AddMinutes(-ageMinutes),
|
||||
Attributes = JsonSerializer.SerializeToElement(new { unit_of_measurement = "%" }) };
|
||||
var options = Options.Create(new HomeAssistantOptions
|
||||
{
|
||||
Entities = new() { Dehumidifier = Dehumidifier, Humidity = Humidity },
|
||||
SleepScene = new() { TargetHumidity = 55 }
|
||||
});
|
||||
var runner = new DeviceActionRunner(gateway, clock, new() { VerificationDelay = TimeSpan.Zero });
|
||||
var action = new BedPresenceDehumidifierStartAction(new DehumidifierModule(runner, options), runner, options, clock);
|
||||
|
||||
Assert.Equal("target-not-configured", await action.ExecuteAsync("humidifier.other", default));
|
||||
Assert.Equal(result, await action.ExecuteAsync(Dehumidifier, default));
|
||||
Assert.Equal(commandCount, gateway.Calls.Count);
|
||||
Assert.All(gateway.Calls, command =>
|
||||
{
|
||||
Assert.Equal(Dehumidifier, command.EntityId);
|
||||
Assert.Equal("humidifier", command.Domain);
|
||||
Assert.Equal("turn_on", command.Service);
|
||||
Assert.Equal("entity_id", Assert.Single(command.Data!).Key);
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("closed", "already-closed")]
|
||||
[InlineData("closing", "already-closing")]
|
||||
|
||||
Reference in New Issue
Block a user