From 0c5e09dcacd46db7b6a62794582035bf7ca76fc7 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 8 Nov 2023 13:00:54 -0500 Subject: [PATCH] agent status on page updates in real time --- OpenAlprWebhookProcessor/Data/Agent.cs | 2 + .../20231108155619_heartbeatms.Designer.cs | 574 ++++++++++++++++++ .../Migrations/20231108155619_heartbeatms.cs | 29 + .../ProcessorContextModelSnapshot.cs | 3 + OpenAlprWebhookProcessor/Settings/Agent.cs | 2 + .../Settings/AgentStatus.cs | 2 + .../GetAgent/GetAgentRequestHandler.cs | 2 +- .../GetAgentStatusRequestHandler.cs | 15 +- .../OpenAlprWebsocketClient.cs | 11 +- .../WebsocketClientOrganizer.cs | 13 +- .../WebhookProcessor/WebhookController.cs | 8 + .../src/app/settings/openalpr-agent/agent.ts | 1 + .../openalpr-agent.component.html | 1 + .../openalpr-agent.component.ts | 115 ++-- .../src/app/signalr/signalr.service.ts | 9 +- 15 files changed, 727 insertions(+), 60 deletions(-) create mode 100644 OpenAlprWebhookProcessor/Data/Migrations/20231108155619_heartbeatms.Designer.cs create mode 100644 OpenAlprWebhookProcessor/Data/Migrations/20231108155619_heartbeatms.cs diff --git a/OpenAlprWebhookProcessor/Data/Agent.cs b/OpenAlprWebhookProcessor/Data/Agent.cs index 130709b2..62fac5f1 100644 --- a/OpenAlprWebhookProcessor/Data/Agent.cs +++ b/OpenAlprWebhookProcessor/Data/Agent.cs @@ -33,5 +33,7 @@ public class Agent public bool IsDebugEnabled { get; set; } public bool IsImageCompressionEnabled { get; set; } + + public long LastHeartbeatEpochMs { get; set; } } } diff --git a/OpenAlprWebhookProcessor/Data/Migrations/20231108155619_heartbeatms.Designer.cs b/OpenAlprWebhookProcessor/Data/Migrations/20231108155619_heartbeatms.Designer.cs new file mode 100644 index 00000000..b419d0a4 --- /dev/null +++ b/OpenAlprWebhookProcessor/Data/Migrations/20231108155619_heartbeatms.Designer.cs @@ -0,0 +1,574 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using OpenAlprWebhookProcessor.Data; + +#nullable disable + +namespace OpenAlprWebhookProcessor.Migrations +{ + [DbContext(typeof(ProcessorContext))] + [Migration("20231108155619_heartbeatms")] + partial class heartbeatms + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.13"); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.Agent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("EndpointUrl") + .HasColumnType("TEXT"); + + b.Property("Hostname") + .HasColumnType("TEXT"); + + b.Property("IsDebugEnabled") + .HasColumnType("INTEGER"); + + b.Property("IsImageCompressionEnabled") + .HasColumnType("INTEGER"); + + b.Property("LastHeartbeatEpochMs") + .HasColumnType("INTEGER"); + + b.Property("LastSuccessfulScrapeEpoch") + .HasColumnType("INTEGER"); + + b.Property("Latitude") + .HasColumnType("REAL"); + + b.Property("Longitude") + .HasColumnType("REAL"); + + b.Property("OpenAlprWebServerApiKey") + .HasColumnType("TEXT"); + + b.Property("OpenAlprWebServerUrl") + .HasColumnType("TEXT"); + + b.Property("SunriseOffset") + .HasColumnType("INTEGER"); + + b.Property("SunsetOffset") + .HasColumnType("INTEGER"); + + b.Property("TimeZoneOffset") + .HasColumnType("REAL"); + + b.Property("Uid") + .HasColumnType("TEXT"); + + b.Property("Version") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Agents"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.Alert", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsStrictMatch") + .HasColumnType("INTEGER"); + + b.Property("PlateNumber") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Alerts"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.Camera", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CameraPassword") + .HasColumnType("TEXT"); + + b.Property("CameraUsername") + .HasColumnType("TEXT"); + + b.Property("DayFocus") + .HasColumnType("TEXT"); + + b.Property("DayZoom") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasColumnType("TEXT"); + + b.Property("LatestProcessedPlateUuid") + .HasColumnType("TEXT"); + + b.Property("Latitude") + .HasColumnType("REAL"); + + b.Property("Longitude") + .HasColumnType("REAL"); + + b.Property("Manufacturer") + .HasColumnType("INTEGER"); + + b.Property("ModelNumber") + .HasColumnType("TEXT"); + + b.Property("NextClearOverlayScheduleId") + .HasColumnType("TEXT"); + + b.Property("NextDayNightScheduleId") + .HasColumnType("TEXT"); + + b.Property("NightFocus") + .HasColumnType("TEXT"); + + b.Property("NightZoom") + .HasColumnType("TEXT"); + + b.Property("OpenAlprCameraId") + .HasColumnType("INTEGER"); + + b.Property("OpenAlprEnabled") + .HasColumnType("INTEGER"); + + b.Property("OpenAlprName") + .HasColumnType("TEXT"); + + b.Property("PlatesSeen") + .HasColumnType("INTEGER"); + + b.Property("SunriseOffset") + .HasColumnType("INTEGER"); + + b.Property("SunsetOffset") + .HasColumnType("INTEGER"); + + b.Property("TimezoneOffset") + .HasColumnType("REAL"); + + b.Property("UpdateDayNightModeEnabled") + .HasColumnType("INTEGER"); + + b.Property("UpdateDayNightModeUrl") + .HasColumnType("TEXT"); + + b.Property("UpdateOverlayEnabled") + .HasColumnType("INTEGER"); + + b.Property("UpdateOverlayTextUrl") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Cameras"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.Enricher", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .HasColumnType("TEXT"); + + b.Property("EnricherType") + .HasColumnType("INTEGER"); + + b.Property("EnrichmentType") + .HasColumnType("INTEGER"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Enrichers"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.Ignore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("IsStrictMatch") + .HasColumnType("INTEGER"); + + b.Property("PlateNumber") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Ignores"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.PlateGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("AgentImageScrapeOccurredOn") + .HasColumnType("REAL"); + + b.Property("AlertDescription") + .HasColumnType("TEXT"); + + b.Property("BestNumber") + .HasColumnType("TEXT"); + + b.Property("Confidence") + .HasColumnType("REAL"); + + b.Property("Direction") + .HasColumnType("REAL"); + + b.Property("IsAlert") + .HasColumnType("INTEGER"); + + b.Property("IsEnriched") + .HasColumnType("INTEGER"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OpenAlprCameraId") + .HasColumnType("INTEGER"); + + b.Property("OpenAlprProcessingTimeMs") + .HasColumnType("REAL"); + + b.Property("OpenAlprUuid") + .HasColumnType("TEXT"); + + b.Property("PlateCoordinates") + .HasColumnType("TEXT"); + + b.Property("ReceivedOnEpoch") + .HasColumnType("INTEGER"); + + b.Property("VehicleColor") + .HasColumnType("TEXT"); + + b.Property("VehicleMake") + .HasColumnType("TEXT"); + + b.Property("VehicleMakeModel") + .HasColumnType("TEXT"); + + b.Property("VehicleRegion") + .HasColumnType("TEXT"); + + b.Property("VehicleType") + .HasColumnType("TEXT"); + + b.Property("VehicleYear") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OpenAlprUuid"); + + b.HasIndex("ReceivedOnEpoch"); + + b.HasIndex("VehicleColor"); + + b.HasIndex("VehicleMake"); + + b.HasIndex("VehicleMakeModel"); + + b.HasIndex("VehicleRegion"); + + b.HasIndex("VehicleType"); + + b.HasIndex("VehicleYear"); + + b.HasIndex("BestNumber", "ReceivedOnEpoch"); + + b.ToTable("PlateGroups"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.PlateGroupPossibleNumbers", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("TEXT"); + + b.Property("PlateGroupId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Number"); + + b.HasIndex("PlateGroupId", "Number"); + + b.ToTable("PlateGroupPossibleNumbers"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.PlateGroupRaw", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("PlateGroupId") + .HasColumnType("TEXT"); + + b.Property("RawPlateGroup") + .HasColumnType("TEXT"); + + b.Property("ReceivedOnEpoch") + .HasColumnType("INTEGER"); + + b.Property("WasProcessedCorrectly") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RawPlateGroups"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.PlateImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsCompressed") + .HasColumnType("INTEGER"); + + b.Property("Jpeg") + .HasColumnType("BLOB"); + + b.Property("PlateGroupId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id"); + + b.HasIndex("PlateGroupId") + .IsUnique(); + + b.ToTable("PlateImage"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.Pushover", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ApiToken") + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("SendPlatePreview") + .HasColumnType("INTEGER"); + + b.Property("UserKey") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("PushoverAlertClients"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.VehicleImage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsCompressed") + .HasColumnType("INTEGER"); + + b.Property("Jpeg") + .HasColumnType("BLOB"); + + b.Property("PlateGroupId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PlateGroupId") + .IsUnique(); + + b.ToTable("VehicleImage"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.WebPushSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("IsEnabled") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("Subject") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WebPushSettings"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.WebPushSubscription", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Endpoint") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("WebPushSubscriptions"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.WebPushSubscriptionKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MobilePushSubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MobilePushSubscriptionId"); + + b.ToTable("WebPushSubscriptionKeys"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.WebhookForward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("ForwardGroupPreviews") + .HasColumnType("INTEGER"); + + b.Property("ForwardGroups") + .HasColumnType("INTEGER"); + + b.Property("ForwardSinglePlates") + .HasColumnType("INTEGER"); + + b.Property("FowardingDestination") + .HasColumnType("TEXT"); + + b.Property("IgnoreSslErrors") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("WebhookForwards"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.PlateGroupPossibleNumbers", b => + { + b.HasOne("OpenAlprWebhookProcessor.Data.PlateGroup", "PlateGroup") + .WithMany("PossibleNumbers") + .HasForeignKey("PlateGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlateGroup"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.PlateImage", b => + { + b.HasOne("OpenAlprWebhookProcessor.Data.PlateGroup", "PlateGroup") + .WithOne("PlateImage") + .HasForeignKey("OpenAlprWebhookProcessor.Data.PlateImage", "PlateGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlateGroup"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.VehicleImage", b => + { + b.HasOne("OpenAlprWebhookProcessor.Data.PlateGroup", "PlateGroup") + .WithOne("VehicleImage") + .HasForeignKey("OpenAlprWebhookProcessor.Data.VehicleImage", "PlateGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PlateGroup"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.WebPushSubscriptionKey", b => + { + b.HasOne("OpenAlprWebhookProcessor.Data.WebPushSubscription", "MobilePushSubscription") + .WithMany("Keys") + .HasForeignKey("MobilePushSubscriptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("MobilePushSubscription"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.PlateGroup", b => + { + b.Navigation("PlateImage"); + + b.Navigation("PossibleNumbers"); + + b.Navigation("VehicleImage"); + }); + + modelBuilder.Entity("OpenAlprWebhookProcessor.Data.WebPushSubscription", b => + { + b.Navigation("Keys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/OpenAlprWebhookProcessor/Data/Migrations/20231108155619_heartbeatms.cs b/OpenAlprWebhookProcessor/Data/Migrations/20231108155619_heartbeatms.cs new file mode 100644 index 00000000..6a155a90 --- /dev/null +++ b/OpenAlprWebhookProcessor/Data/Migrations/20231108155619_heartbeatms.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace OpenAlprWebhookProcessor.Migrations +{ + /// + public partial class heartbeatms : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "LastHeartbeatEpochMs", + table: "Agents", + type: "INTEGER", + nullable: false, + defaultValue: 0L); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "LastHeartbeatEpochMs", + table: "Agents"); + } + } +} diff --git a/OpenAlprWebhookProcessor/Data/Migrations/ProcessorContextModelSnapshot.cs b/OpenAlprWebhookProcessor/Data/Migrations/ProcessorContextModelSnapshot.cs index fea49da5..e8c3f80c 100644 --- a/OpenAlprWebhookProcessor/Data/Migrations/ProcessorContextModelSnapshot.cs +++ b/OpenAlprWebhookProcessor/Data/Migrations/ProcessorContextModelSnapshot.cs @@ -35,6 +35,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("IsImageCompressionEnabled") .HasColumnType("INTEGER"); + b.Property("LastHeartbeatEpochMs") + .HasColumnType("INTEGER"); + b.Property("LastSuccessfulScrapeEpoch") .HasColumnType("INTEGER"); diff --git a/OpenAlprWebhookProcessor/Settings/Agent.cs b/OpenAlprWebhookProcessor/Settings/Agent.cs index e12de68a..80f85f68 100644 --- a/OpenAlprWebhookProcessor/Settings/Agent.cs +++ b/OpenAlprWebhookProcessor/Settings/Agent.cs @@ -27,5 +27,7 @@ public class Agent public bool IsDebugEnabled { get; set; } public bool IsImageCompressionEnabled { get; set; } + + public long LastHeartbeatEpochMs { get; set; } } } diff --git a/OpenAlprWebhookProcessor/Settings/AgentStatus.cs b/OpenAlprWebhookProcessor/Settings/AgentStatus.cs index b445da77..400286c2 100644 --- a/OpenAlprWebhookProcessor/Settings/AgentStatus.cs +++ b/OpenAlprWebhookProcessor/Settings/AgentStatus.cs @@ -21,5 +21,7 @@ public class AgentStatus public long AgentEpochMs { get; set; } public bool AlprdActive { get; set; } + + public long LastHeartbeatEpochMs { get; set; } } } diff --git a/OpenAlprWebhookProcessor/Settings/GetAgent/GetAgentRequestHandler.cs b/OpenAlprWebhookProcessor/Settings/GetAgent/GetAgentRequestHandler.cs index 91f0e4bc..4f85ac90 100644 --- a/OpenAlprWebhookProcessor/Settings/GetAgent/GetAgentRequestHandler.cs +++ b/OpenAlprWebhookProcessor/Settings/GetAgent/GetAgentRequestHandler.cs @@ -1,6 +1,5 @@ using Microsoft.EntityFrameworkCore; using OpenAlprWebhookProcessor.Data; -using OpenAlprWebhookProcessor.WebhookProcessor.OpenAlprWebsocket; using System.Threading; using System.Threading.Tasks; @@ -32,6 +31,7 @@ public async Task HandleAsync(CancellationToken cancellationToken) Hostname = agent.Hostname, IsDebugEnabled = agent.IsDebugEnabled, IsImageCompressionEnabled = agent.IsImageCompressionEnabled, + LastHeartbeatEpochMs = agent.LastHeartbeatEpochMs, Latitude = agent.Latitude, Longitude = agent.Longitude, OpenAlprWebServerApiKey = agent.OpenAlprWebServerApiKey, diff --git a/OpenAlprWebhookProcessor/Settings/GetAgentStatus/GetAgentStatusRequestHandler.cs b/OpenAlprWebhookProcessor/Settings/GetAgentStatus/GetAgentStatusRequestHandler.cs index d3efae9f..df58615d 100644 --- a/OpenAlprWebhookProcessor/Settings/GetAgentStatus/GetAgentStatusRequestHandler.cs +++ b/OpenAlprWebhookProcessor/Settings/GetAgentStatus/GetAgentStatusRequestHandler.cs @@ -23,12 +23,11 @@ public GetAgentStatusRequestHandler( public async Task HandleAsync(CancellationToken cancellationToken) { - var agentUid = await _processorContext.Agents + var agent = await _processorContext.Agents .AsNoTracking() - .Select(x => x.Uid) .FirstOrDefaultAsync(cancellationToken); - if (agentUid == null) + if (agent.Uid == null) { return new AgentStatus() { @@ -36,13 +35,14 @@ public async Task HandleAsync(CancellationToken cancellationToken) }; } - var agentStatus = await _websocketClientOrganizer.GetAgentStatusAsync(agentUid, cancellationToken); + var agentStatus = await _websocketClientOrganizer.GetAgentStatusAsync(agent.Uid, cancellationToken); if (agentStatus == null) { return new AgentStatus() { IsConnected = false, + LastHeartbeatEpochMs = agent.LastHeartbeatEpochMs, }; } @@ -50,14 +50,15 @@ public async Task HandleAsync(CancellationToken cancellationToken) { AgentEpochMs = agentStatus.AgentEpochMs, AlprdActive = agentStatus.AgentStatus.AlprdActive, - Hostname = agentStatus.AgentStatus.AgentHostname, - IsConnected = true, CpuCores = agentStatus.AgentStatus.CpuCores, CpuUsagePercent = agentStatus.AgentStatus.CpuUsagePercent, DaemonUptimeSeconds = agentStatus.AgentStatus.DaemonUptimeSeconds, DiskFreeBytes = agentStatus.AgentStatus.DiskDriveFreeBytes, - Version = agentStatus.Version, + Hostname = agentStatus.AgentStatus.AgentHostname, + IsConnected = true, + LastHeartbeatEpochMs = agent.LastHeartbeatEpochMs, SystemUptimeSeconds = agentStatus.AgentStatus.SystemUptimeSeconds, + Version = agentStatus.Version, }; } } diff --git a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/OpenAlprWebsocketClient.cs b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/OpenAlprWebsocketClient.cs index b4a7ad5e..8b83563e 100644 --- a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/OpenAlprWebsocketClient.cs +++ b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/OpenAlprWebsocketClient.cs @@ -105,10 +105,13 @@ public bool TryGetAgentStatusResponse( public async Task CloseConnectionAsync(CancellationToken cancellationToken) { - await _webSocket.CloseAsync( - WebSocketCloseStatus.NormalClosure, - "goodbye.", - cancellationToken); + if (_webSocket.State == WebSocketState.Open) + { + await _webSocket.CloseAsync( + WebSocketCloseStatus.NormalClosure, + "goodbye.", + cancellationToken); + } } [GeneratedRegex("transaction_id\":\"(.*?)\"")] diff --git a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketClientOrganizer.cs b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketClientOrganizer.cs index 231b2dd1..0cde1bb2 100644 --- a/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketClientOrganizer.cs +++ b/OpenAlprWebhookProcessor/WebhookProcessor/OpenAlprWebsocket/WebsocketClientOrganizer.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; using System.Diagnostics; @@ -11,8 +12,12 @@ public class WebsocketClientOrganizer : BackgroundService { private readonly ConcurrentDictionary _connectedClients; - public WebsocketClientOrganizer() + private readonly ILogger _logger; + + public WebsocketClientOrganizer( + ILogger logger) { + _logger = logger; _connectedClients = new ConcurrentDictionary(); } @@ -56,7 +61,8 @@ public async Task GetAgentStatusAsync( if (!agentExists) { - throw new ArgumentException("AgentId is not connected."); + _logger.LogError("AgentId is not connected: {agentId}", agentId); + return null; } var transactionId = Guid.NewGuid(); @@ -76,7 +82,8 @@ public async Task GetAgentStatusAsync( await Task.Delay(1000, cancellationToken); } - throw new TimeoutException("Agent did not respond to request."); + _logger.LogError("Agent did not respond to request."); + return null; } } } diff --git a/OpenAlprWebhookProcessor/WebhookProcessor/WebhookController.cs b/OpenAlprWebhookProcessor/WebhookProcessor/WebhookController.cs index c2892c42..fd956cd5 100644 --- a/OpenAlprWebhookProcessor/WebhookProcessor/WebhookController.cs +++ b/OpenAlprWebhookProcessor/WebhookProcessor/WebhookController.cs @@ -1,7 +1,9 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using OpenAlprWebhookProcessor.Data; using OpenAlprWebhookProcessor.WebhookProcessor.OpenAlprWebhook; +using System; using System.IO; using System.Text; using System.Text.Json; @@ -88,6 +90,12 @@ await _singlePlateWebhookHandler.HandleWebhookAsync( else if (rawWebhook.Contains("heartbeat")) { _logger.LogInformation("received heartbeat from agent"); + + var agent = await _processorContext.Agents + .FirstOrDefaultAsync(cancellationToken); + + agent.LastSuccessfulScrapeEpoch = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + await _processorContext.SaveChangesAsync(); } else { diff --git a/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/agent.ts b/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/agent.ts index d7ca3373..f6ceac0a 100644 --- a/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/agent.ts +++ b/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/agent.ts @@ -3,6 +3,7 @@ export class Agent { hostname: string; isDebugEnabled: boolean; isImageCompressionEnabled: boolean; + lastHeartbeatEpochMs: number; latitude: number; longitude: number; uid: string; diff --git a/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.html b/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.html index f65f1be8..61a55062 100644 --- a/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.html +++ b/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.html @@ -9,6 +9,7 @@ {{agentStatus.isConnected ? "Connected" : "Disconnected"}} diff --git a/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.ts b/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.ts index 0f061df6..66ec66f2 100644 --- a/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.ts +++ b/OpenAlprWebhookProcessor/angularapp/src/app/settings/openalpr-agent/openalpr-agent.component.ts @@ -1,17 +1,22 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { SnackbarService } from 'app/snackbar/snackbar.service'; import { SnackBarType } from 'app/snackbar/snackbartype'; import { SettingsService } from '../settings.service'; import { Agent } from './agent'; import { AgentStatus } from './agentStatus'; import { PlateStatisticsData } from 'app/plates/plate/plateStatistics'; +import { SignalrService } from 'app/signalr/signalr.service'; +import { Subscription } from 'rxjs'; +import { MatTable } from '@angular/material/table'; @Component({ selector: 'app-openalpr-agent', templateUrl: './openalpr-agent.component.html', styleUrls: ['./openalpr-agent.component.less'] }) -export class OpenalprAgentComponent implements OnInit { +export class OpenalprAgentComponent implements OnInit, OnDestroy { + @ViewChild('agentStatusTable') table: MatTable; + public agent: Agent; public agentStatus: AgentStatus; public agentStatusData: PlateStatisticsData[] = []; @@ -20,13 +25,24 @@ export class OpenalprAgentComponent implements OnInit { public isSaving: boolean = false; public isHydrating: boolean = false; + private eventSubscriptions = new Subscription(); + constructor( private settingsService: SettingsService, - private snackBarService: SnackbarService) { } + private snackBarService: SnackbarService, + private signalRHub: SignalrService) { } ngOnInit(): void { this.getAgent(); this.getAgentStatus(); + + this.eventSubscriptions.add(this.signalRHub.openAlprAgentConnectionStatusChanged.subscribe((connected: boolean) => { + this.getAgentStatus(); + })); + } + + ngOnDestroy(): void { + this.eventSubscriptions.unsubscribe(); } public saveAgent() { @@ -56,49 +72,62 @@ export class OpenalprAgentComponent implements OnInit { private getAgentStatus() { this.settingsService.getAgentStatus().subscribe(result => { this.agentStatus = result; - - this.agentStatusData.push({ - key: "Cpu Cores", - value: this.agentStatus.cpuCores.toString(), - }); - - this.agentStatusData.push({ - key: "Cpu Usage", - value: this.agentStatus.cpuUsagePercent.toString() + "%", - }); - - this.agentStatusData.push({ - key: "ALPR Daemon Active", - value: this.agentStatus.alprdActive ? "Yes" : "No", - }); - - this.agentStatusData.push({ - key: "Daemon Uptime", - value: this.agentStatus.daemonUptimeSeconds.toString() + " seconds", - }); - - this.agentStatusData.push({ - key: "Free Disk Space", - value: this.formatBytes(this.agentStatus.diskFreeBytes), - }); - - this.agentStatusData.push({ - key: "Hostname", - value: this.agentStatus.hostname, - }); - - this.agentStatusData.push({ - key: "Current Time", - value: (new Date(this.agentStatus.agentEpochMs)).toString(), - }); - - this.agentStatusData.push({ - key: "Version", - value: this.agentStatus.version, - }); + this.agentStatusData = new Array(); + + if (this.agentStatus.isConnected) { + this.agentStatusData.push({ + key: "Cpu Cores", + value: this.agentStatus.cpuCores.toString(), + }); + + this.agentStatusData.push({ + key: "Cpu Usage", + value: this.agentStatus.cpuUsagePercent.toString() + "%", + }); + + this.agentStatusData.push({ + key: "ALPR Daemon Active", + value: this.agentStatus.alprdActive ? "Yes" : "No", + }); + + this.agentStatusData.push({ + key: "Daemon Uptime", + value: this.agentStatus.daemonUptimeSeconds.toString() + " seconds", + }); + + this.agentStatusData.push({ + key: "Free Disk Space", + value: this.formatBytes(this.agentStatus.diskFreeBytes), + }); + + this.agentStatusData.push({ + key: "Hostname", + value: this.agentStatus.hostname, + }); + + this.agentStatusData.push({ + key: "Current Time", + value: (new Date(this.agentStatus.agentEpochMs)).toString(), + }); + + this.agentStatusData.push({ + key: "Version", + value: this.agentStatus.version, + }); + } else { + this.agentStatusData.push({ + key: "Last Heartbeat", + value: (new Date(this.agent.lastHeartbeatEpochMs)).toString(), + }); + } + + this.table.renderRows(); }, (error) => { + this.agentStatus = new AgentStatus(); this.agentStatus.isConnected = false; + this.agentStatusData = new Array(); + this.table.renderRows(); }); } diff --git a/OpenAlprWebhookProcessor/angularapp/src/app/signalr/signalr.service.ts b/OpenAlprWebhookProcessor/angularapp/src/app/signalr/signalr.service.ts index 935ba609..8dcde5d3 100644 --- a/OpenAlprWebhookProcessor/angularapp/src/app/signalr/signalr.service.ts +++ b/OpenAlprWebhookProcessor/angularapp/src/app/signalr/signalr.service.ts @@ -14,7 +14,7 @@ export class SignalrService { public licensePlateReceived = new Subject(); public licensePlateAlerted = new Subject(); public processInformationLogged = new Subject(); - public openAlprAgentConnected = new Subject(); + public openAlprAgentConnectionStatusChanged = new Subject(); public isConnected: boolean; public connectionStatusChanged: Subject = new Subject(); @@ -45,10 +45,15 @@ export class SignalrService { }); this.hubConnection.on('OpenAlprAgentConnected', (agentId, ipAddress) => { - this.snackbarService.create(`OpenALPR Agent Connected: ${agentId}`, SnackBarType.Connected, `IP Address: ${ipAddress}`); + this.openAlprAgentConnectionStatusChanged.next(true); + this.snackbarService.create( + `OpenALPR Agent Connected: ${agentId}`, + SnackBarType.Connected, + `IP Address: ${ipAddress}`); }); this.hubConnection.on('OpenAlprAgentDisconnected', (agentId, ipAddress) => { + this.openAlprAgentConnectionStatusChanged.next(false); this.snackbarService.create( `OpenALPR Agent Disconnected: ${agentId}`, SnackBarType.Disconnected,