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,