using System.Net.Http.Headers;
using Microsoft.Extensions.Logging;
namespace MeterVault.Infrastructure.Ingestion;
/// Which way a Home Assistant connectivity test ended.
///
/// The verdict is shown to whoever clicked "Test connection", so it has to be sayable in their
/// language — and this assembly has no business knowing what that is. The outcome travels as a
/// value and the admin UI supplies the words.
///
public enum HaTestOutcome
{
///
/// No test was run — the caller decided beforehand (no token configured, base URL edited since
/// saving) and put its own, already-localized wording in .
/// The default, so a result built by the UI needs no ceremony to say so.
///
Precondition,
/// Reachable and the token was accepted; no entity was named to sample.
Connected,
/// Reachable and the named entity returned a number — see .
ConnectedWithValue,
/// No base URL was given.
BaseUrlMissing,
/// No token was available to test with.
TokenMissing,
/// HA answered, but not with success. carries the status.
HttpError,
/// Reachable, but the named entity has no numeric state (unavailable/unknown/non-numeric).
NoNumericState,
/// The request threw. carries the exception message.
RequestFailed,
}
/// Outcome of a Home Assistant connectivity test.
/// The English summary, kept for logs and non-UI callers.
/// The same verdict as a value, for a UI that has to phrase it in some language.
/// The entity that was sampled, when one was named.
/// Diagnostic text (HTTP status, exception message). Not ours to translate.
public sealed record HaTestResult(
bool Ok,
string Message,
double? SampleValue = null,
HaTestOutcome Outcome = HaTestOutcome.Precondition,
string? EntityId = null,
string? Detail = null);
///
/// Verifies a Home Assistant connection from the admin UI: checks the base URL + token against
/// GET /api/, and optionally reads one entity's state. Confirms the app can actually read HA
/// before a source is relied upon (SDD §6.2).
///
///
/// Takes the token already resolved rather than a reference to one. The admin UI must be able to
/// test a token that has been typed but not yet saved (so not yet encrypted), and keeping the two
/// storage forms out of here leaves one resolution path in .
///
public sealed class HaConnectionTester(IHttpClientFactory httpClientFactory, ILogger logger)
{
private readonly IHttpClientFactory _httpClientFactory = httpClientFactory;
private readonly ILogger _logger = logger;
public async Task TestAsync(
string? baseUrl, string? token, string? entityId, CancellationToken cancellationToken = default)
{
if (string.IsNullOrWhiteSpace(baseUrl))
{
return new HaTestResult(false, "Base URL is required.", Outcome: HaTestOutcome.BaseUrlMissing);
}
if (string.IsNullOrWhiteSpace(token))
{
return new HaTestResult(false, "No token available to test.", Outcome: HaTestOutcome.TokenMissing);
}
var client = _httpClientFactory.CreateClient();
client.Timeout = TimeSpan.FromSeconds(10);
try
{
using var request = new HttpRequestMessage(HttpMethod.Get, $"{baseUrl.TrimEnd('/')}/api/");
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
return new HaTestResult(false, $"HA returned {(int)response.StatusCode} {response.ReasonPhrase}.",
Outcome: HaTestOutcome.HttpError,
Detail: $"{(int)response.StatusCode} {response.ReasonPhrase}".Trim());
}
if (string.IsNullOrWhiteSpace(entityId))
{
return new HaTestResult(true, "Connected — Home Assistant API reachable and token accepted.",
Outcome: HaTestOutcome.Connected);
}
var state = await new HaStateClient(client).GetStateAsync(baseUrl, token, entityId, null, cancellationToken).ConfigureAwait(false);
return state is { } value
? new HaTestResult(true, $"Connected — {entityId} = {value.Value}.", value.Value,
HaTestOutcome.ConnectedWithValue, entityId)
: new HaTestResult(false, $"Connected, but '{entityId}' has no numeric state (unavailable/unknown or non-numeric).",
Outcome: HaTestOutcome.NoNumericState, EntityId: entityId);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Home Assistant connection test failed for {BaseUrl}", baseUrl);
return new HaTestResult(false, $"Connection failed: {ex.Message}",
Outcome: HaTestOutcome.RequestFailed, Detail: ex.Message);
}
}
}