using System.Net.Http.Headers; using Microsoft.Extensions.Logging; namespace MeterVault.Infrastructure.Ingestion; /// Outcome of a Home Assistant connectivity test. public sealed record HaTestResult(bool Ok, string Message, double? SampleValue = null); /// /// Verifies a Home Assistant connection from the admin UI: checks the base URL + resolved 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). Token is resolved by reference (env var). /// 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? tokenEnv, string? entityId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(baseUrl)) { return new HaTestResult(false, "Base URL is required."); } if (string.IsNullOrWhiteSpace(tokenEnv)) { return new HaTestResult(false, "Token env-var name is required (the token is resolved from it at runtime)."); } var token = Environment.GetEnvironmentVariable(tokenEnv); if (string.IsNullOrWhiteSpace(token)) { return new HaTestResult(false, $"Environment variable '{tokenEnv}' is not set on the server."); } 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}."); } if (string.IsNullOrWhiteSpace(entityId)) { return new HaTestResult(true, "Connected — Home Assistant API reachable and token accepted."); } 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) : new HaTestResult(false, $"Connected, but '{entityId}' has no numeric state (unavailable/unknown or non-numeric)."); } catch (Exception ex) { _logger.LogWarning(ex, "Home Assistant connection test failed for {BaseUrl}", baseUrl); return new HaTestResult(false, $"Connection failed: {ex.Message}"); } } }