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 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 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 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 states, string entityId) => states.FirstOrDefault(state => string.Equals(state.EntityId, entityId, StringComparison.OrdinalIgnoreCase)); }