using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; using ZB.MOM.WW.OtOpcUa.Configuration; using ZB.MOM.WW.OtOpcUa.Configuration.Entities; namespace ZB.MOM.WW.OtOpcUa.Admin.Services; /// /// Per-instance detail view for FOCAS driver rows. Loads the latest /// row for the requested DriverInstanceId (most-recent /// draft wins when multiple rows exist across generations), parses the schemaless /// DriverConfig JSON into , and joins the /// per-device rows so the Admin page can render host /// state + consecutive-failure counters next to each configured device. /// public sealed class FocasDriverDetailService(OtOpcUaConfigDbContext db) { private static readonly JsonSerializerOptions JsonOpts = new() { PropertyNameCaseInsensitive = true, NumberHandling = JsonNumberHandling.AllowReadingFromString, }; public async Task GetAsync(string driverInstanceId, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(driverInstanceId)) return null; var instance = await db.DriverInstances.AsNoTracking() .Where(d => d.DriverInstanceId == driverInstanceId && d.DriverType.ToLower() == "focas") .OrderByDescending(d => d.GenerationId) .FirstOrDefaultAsync(ct); if (instance is null) return null; FocasDriverConfigView? config = null; string? parseError = null; try { config = JsonSerializer.Deserialize(instance.DriverConfig, JsonOpts); } catch (JsonException ex) { parseError = ex.Message; } var hostStatuses = await (from s in db.DriverHostStatuses.AsNoTracking() where s.DriverInstanceId == driverInstanceId join r in db.DriverInstanceResilienceStatuses.AsNoTracking() on new { s.DriverInstanceId, s.HostName } equals new { r.DriverInstanceId, r.HostName } into rj from r in rj.DefaultIfEmpty() orderby s.HostName select new FocasHostStatusRow( s.NodeId, s.HostName, s.State.ToString(), s.StateChangedUtc, s.LastSeenUtc, s.Detail, r != null ? r.ConsecutiveFailures : 0, r != null ? r.LastCircuitBreakerOpenUtc : null, r != null ? r.LastRecycleUtc : null)).ToListAsync(ct); return new FocasDriverDetail(instance, config, parseError, hostStatuses); } } /// Projected view of a FOCAS driver's parsed config. Unknown fields are ignored. public sealed record FocasDriverConfigView { public List? Devices { get; set; } public List? Tags { get; set; } public FocasProbeView? Probe { get; set; } public FocasAlarmProjectionView? AlarmProjection { get; set; } public FocasHandleRecycleView? HandleRecycle { get; set; } } public sealed record FocasDeviceView { public string? HostAddress { get; set; } public string? DeviceName { get; set; } public string? Series { get; set; } } public sealed record FocasTagView { public string? Name { get; set; } public string? DeviceHostAddress { get; set; } public string? Address { get; set; } public string? DataType { get; set; } public bool Writable { get; set; } = true; } public sealed record FocasProbeView { public bool Enabled { get; set; } = true; public string? Interval { get; set; } } public sealed record FocasAlarmProjectionView { public bool Enabled { get; set; } public string? PollInterval { get; set; } } public sealed record FocasHandleRecycleView { public bool Enabled { get; set; } public string? Interval { get; set; } } /// Composite payload returned to the Admin page. public sealed record FocasDriverDetail( DriverInstance Instance, FocasDriverConfigView? Config, string? ParseError, IReadOnlyList HostStatuses); public sealed record FocasHostStatusRow( string NodeId, string HostName, string State, DateTime StateChangedUtc, DateTime LastSeenUtc, string? Detail, int ConsecutiveFailures, DateTime? LastCircuitBreakerOpenUtc, DateTime? LastRecycleUtc);