diff --git a/OrcanodeMonitor/Core/Fetcher.cs b/OrcanodeMonitor/Core/Fetcher.cs index 7eafbc8..2fbd1c6 100644 --- a/OrcanodeMonitor/Core/Fetcher.cs +++ b/OrcanodeMonitor/Core/Fetcher.cs @@ -30,6 +30,7 @@ public class Fetcher private static HttpClient _httpClient = new HttpClient(); private static string _orcasoundFeedsUrl = "https://live.orcasound.net/api/json/feeds"; private static string _dataplicityDevicesUrl = "https://apps.dataplicity.com/devices/"; + private static string _orcaHelloHydrophonesUrl = "https://aifororcasdetections2.azurewebsites.net/api/hydrophones"; private static DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); private static string _iftttServiceKey = Environment.GetEnvironmentVariable("IFTTT_SERVICE_KEY") ?? ""; private static string _defaultS3Bucket = "streaming-orcasound-net"; @@ -55,13 +56,12 @@ private static bool NameMatch(string name, string dataplicityName) /// /// Find a node using the serial number value at Dataplicity. /// - /// Database table to look in and potentially update + /// Database table to look in and potentially update /// Dataplicity serial number to look for /// Dataplicity connection status /// - private static Orcanode? FindOrcanodeByDataplicitySerial(DbSet nodeList, string serial, out OrcanodeOnlineStatus connectionStatus) + private static Orcanode? FindOrcanodeByDataplicitySerial(List nodes, string serial, out OrcanodeOnlineStatus connectionStatus) { - List nodes = nodeList.ToList(); foreach (Orcanode node in nodes) { if (node.DataplicitySerial == serial) @@ -96,7 +96,7 @@ private static Orcanode CreateOrcanode(DbSet nodeList) /// private static Orcanode FindOrCreateOrcanodeByDataplicitySerial(DbSet nodeList, string serial, out OrcanodeOnlineStatus connectionStatus) { - Orcanode? node = FindOrcanodeByDataplicitySerial(nodeList, serial, out connectionStatus); + Orcanode? node = FindOrcanodeByDataplicitySerial(nodeList.ToList(), serial, out connectionStatus); if (node != null) { return node; @@ -108,9 +108,8 @@ private static Orcanode FindOrCreateOrcanodeByDataplicitySerial(DbSet return newNode; } - private static Orcanode? FindOrcanodeByOrcasoundFeedId(DbSet nodeList, string feedId) + private static Orcanode? FindOrcanodeByOrcasoundFeedId(List nodes, string feedId) { - List nodes = nodeList.ToList(); foreach (Orcanode node in nodes) { if (node.OrcasoundFeedId == feedId) @@ -125,12 +124,11 @@ private static Orcanode FindOrCreateOrcanodeByDataplicitySerial(DbSet /// /// Look for an Orcanode by Orcasound name in a list. /// - /// Orcanode list to look in + /// Orcanode list to look in /// Name to look for /// Node found - private static Orcanode? FindOrcanodeByOrcasoundName(DbSet nodeList, string orcasoundName) + private static Orcanode? FindOrcanodeByOrcasoundName(List nodes, string orcasoundName) { - List nodes = nodeList.ToList(); foreach (Orcanode node in nodes) { if (node.OrcasoundName == orcasoundName) @@ -176,90 +174,103 @@ private static Orcanode FindOrCreateOrcanodeByOrcasoundName(DbSet node return newNode; } -#if ORCAHELLO /// /// Update the list of Orcanodes using data from OrcaHello. - /// OrcaHello does not currently allow enumerating nodes. /// /// Database context to update /// public async static Task UpdateOrcaHelloDataAsync(OrcanodeMonitorContext context) { - List nodes = await context.Orcanodes.ToListAsync(); - foreach (Orcanode node in nodes) - { - await UpdateOrcaHelloDataAsync(context, node); - } - } - - public async static Task UpdateOrcaHelloDataAsync(OrcanodeMonitorContext context, Orcanode node) - { - string? name = HttpUtility.UrlEncode(node.OrcaHelloName); - if (name == null) - { - return; - } - string url = "https://aifororcasdetections.azurewebsites.net/api/detections?Page=1&SortBy=timestamp&SortOrder=desc&Timeframe=all&Location=" + name + "&RecordsPerPage=1"; - string json = ""; try { - json = await _httpClient.GetStringAsync(url); - if (json == "") + string json = await _httpClient.GetStringAsync(_orcaHelloHydrophonesUrl); + if (json.IsNullOrEmpty()) { return; } - dynamic dataArray = JsonSerializer.Deserialize(json); - if (dataArray.ValueKind != JsonValueKind.Array) + dynamic response = JsonSerializer.Deserialize(json); + if (response == null) + { + return; + } + JsonElement hydrophoneArray = response.hydrophones; + if (hydrophoneArray.ValueKind != JsonValueKind.Array) { return; } - foreach (JsonElement detection in dataArray.EnumerateArray()) + + // Get a snapshot to use during the loop to avoid multiple queries. + var foundList = context.Orcanodes.ToList(); + + // Create a list to track what nodes are no longer returned. + var unfoundList = foundList.ToList(); + + foreach (JsonElement hydrophone in hydrophoneArray.EnumerateArray()) { - if (!detection.TryGetProperty("timestamp", out var timestampElement)) + // "id" holds the OrcaHello id which is also the S3NodeName. + if (!hydrophone.TryGetProperty("id", out var hydrophoneId)) { - return; + continue; + } + + // "name" holds the display name. + if (!hydrophone.TryGetProperty("name", out var name)) + { + continue; + } + + // Remove the returned node from the unfound list. + Orcanode? oldListNode = unfoundList.Find(a => a.OrcaHelloId == hydrophoneId.ToString()); + if (oldListNode == null) + { + oldListNode = unfoundList.Find(a => a.S3NodeName == hydrophoneId.ToString()); } - if (!DateTime.TryParseExact(timestampElement.ToString(), "yyyy-MM-ddTHH:mm:ss.ffffffZ", System.Globalization.CultureInfo.InvariantCulture, System.Globalization.DateTimeStyles.AssumeUniversal, out DateTime timestamp)) + if (oldListNode == null) { - return; + oldListNode = unfoundList.Find(a => a.OrcasoundName == name.ToString()); } - if (timestamp <= node.LastOrcaHelloDetectionTimestamp) + if (oldListNode != null) { - // No new detections. - return; + unfoundList.Remove(oldListNode); } - // Parse other properties. - if (!detection.TryGetProperty("confidence", out var confidenceElement)) + // TODO: we should have a unique id, independent of S3. + Orcanode? node = FindOrcanodeByOrcasoundName(foundList, name.ToString()); + if (node == null) { - return; + node = CreateOrcanode(context.Orcanodes); + node.OrcasoundName = name.ToString(); + node.S3NodeName = hydrophoneId.ToString(); } - if (!confidenceElement.TryGetDouble(out double confidence)) + + node.OrcaHelloId = hydrophoneId.ToString(); + } + + // Mark any remaining unfound nodes as absent. + foreach (var unfoundNode in unfoundList) + { + Orcanode? oldNode = null; + if (!unfoundNode.OrcasoundName.IsNullOrEmpty()) { - return; + oldNode = FindOrcanodeByOrcasoundName(foundList, unfoundNode.OrcasoundName); } - if (!detection.TryGetProperty("comments", out var comments)) + else if (!unfoundNode.DataplicitySerial.IsNullOrEmpty()) { - return; + oldNode = FindOrcanodeByDataplicitySerial(foundList, unfoundNode.DataplicitySerial, out OrcanodeOnlineStatus connectionStatus); } - if (!detection.TryGetProperty("found", out var found)) + if (oldNode != null) { - return; + oldNode.OrcaHelloId = String.Empty; } - - node.LastOrcaHelloDetectionTimestamp = timestamp; - node.LastOrcaHelloDetectionFound = found.ToString() == "yes"; - node.LastOrcaHelloDetectionComments = comments.ToString(); - node.LastOrcaHelloDetectionConfidence = (int)(confidence + 0.5); } await context.SaveChangesAsync(); } catch (Exception ex) { + string msg = ex.ToString(); } } -#endif /// /// Update Orcanode state by querying dataplicity.com. @@ -289,8 +300,10 @@ public async static Task UpdateDataplicityDataAsync(OrcanodeMonitorContext conte jsonArray = await response.Content.ReadAsStringAsync(); } + var foundList = context.Orcanodes.ToList(); + // Create a list to track what nodes are no longer returned. - var unfoundList = context.Orcanodes.ToList(); + var unfoundList = foundList.ToList(); dynamic deviceArray = JsonSerializer.Deserialize(jsonArray); if (deviceArray.ValueKind != JsonValueKind.Array) @@ -388,7 +401,7 @@ public async static Task UpdateDataplicityDataAsync(OrcanodeMonitorContext conte // Mark any remaining unfound nodes as absent. foreach (var unfoundNode in unfoundList) { - var oldNode = FindOrcanodeByDataplicitySerial(context.Orcanodes, unfoundNode.DataplicitySerial, out OrcanodeOnlineStatus unfoundNodeStatus); + var oldNode = FindOrcanodeByDataplicitySerial(foundList, unfoundNode.DataplicitySerial, out OrcanodeOnlineStatus unfoundNodeStatus); if (oldNode != null) { oldNode.DataplicityOnline = null; @@ -414,7 +427,7 @@ public async static Task UpdateOrcasoundDataAsync(OrcanodeMonitorContext context try { string json = await _httpClient.GetStringAsync(_orcasoundFeedsUrl); - if (json == "") + if (json.IsNullOrEmpty()) { return; } @@ -429,8 +442,10 @@ public async static Task UpdateOrcasoundDataAsync(OrcanodeMonitorContext context return; } + var foundList = context.Orcanodes.ToList(); + // Create a list to track what nodes are no longer returned. - var unfoundList = context.Orcanodes.ToList(); + var unfoundList = foundList.ToList(); foreach (JsonElement feed in dataArray.EnumerateArray()) { @@ -466,11 +481,11 @@ public async static Task UpdateOrcasoundDataAsync(OrcanodeMonitorContext context } Orcanode? node = null; - node = FindOrcanodeByOrcasoundFeedId(context.Orcanodes, feedId.ToString()); + node = FindOrcanodeByOrcasoundFeedId(foundList, feedId.ToString()); if (node == null) { // We didn't used to store the feed ID, only the name, so try again by name. - node = FindOrcanodeByOrcasoundName(context.Orcanodes, orcasoundName); + node = FindOrcanodeByOrcasoundName(foundList, orcasoundName); } // See if we can find a node by dataplicity ID, so that if a node @@ -479,7 +494,7 @@ public async static Task UpdateOrcasoundDataAsync(OrcanodeMonitorContext context Orcanode? dataplicityNode = null; if (!dataplicitySerial.IsNullOrEmpty()) { - dataplicityNode = FindOrcanodeByDataplicitySerial(context.Orcanodes, dataplicitySerial, out OrcanodeOnlineStatus oldStatus); + dataplicityNode = FindOrcanodeByDataplicitySerial(foundList, dataplicitySerial, out OrcanodeOnlineStatus oldStatus); if (dataplicityNode != null) { if (node == null) @@ -547,7 +562,7 @@ public async static Task UpdateOrcasoundDataAsync(OrcanodeMonitorContext context // Mark any remaining unfound nodes as absent. foreach (var unfoundNode in unfoundList) { - var oldNode = FindOrcanodeByOrcasoundFeedId(context.Orcanodes, unfoundNode.OrcasoundFeedId); + var oldNode = FindOrcanodeByOrcasoundFeedId(foundList, unfoundNode.OrcasoundFeedId); if (oldNode != null) { oldNode.OrcasoundFeedId = String.Empty; diff --git a/OrcanodeMonitor/Core/PeriodicTasks.cs b/OrcanodeMonitor/Core/PeriodicTasks.cs index 5bb5a1b..0e1e21f 100644 --- a/OrcanodeMonitor/Core/PeriodicTasks.cs +++ b/OrcanodeMonitor/Core/PeriodicTasks.cs @@ -65,10 +65,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) await Fetcher.UpdateS3DataAsync(context); -#if ORCAHELLO - // OrcaHello is time-consuming to query so do this last. await Fetcher.UpdateOrcaHelloDataAsync(context); -#endif } } } diff --git a/OrcanodeMonitor/Migrations/OrcanodeMonitorContextModelSnapshot.cs b/OrcanodeMonitor/Migrations/OrcanodeMonitorContextModelSnapshot.cs index eeb75c1..d914c69 100644 --- a/OrcanodeMonitor/Migrations/OrcanodeMonitorContextModelSnapshot.cs +++ b/OrcanodeMonitor/Migrations/OrcanodeMonitorContextModelSnapshot.cs @@ -89,6 +89,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ManifestUpdatedUtc") .HasColumnType("datetime2"); + b.Property("OrcaHelloId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + b.Property("OrcasoundFeedId") .IsRequired() .HasColumnType("nvarchar(max)"); diff --git a/OrcanodeMonitor/Models/Orcanode.cs b/OrcanodeMonitor/Models/Orcanode.cs index 0b21f41..555ba66 100644 --- a/OrcanodeMonitor/Models/Orcanode.cs +++ b/OrcanodeMonitor/Models/Orcanode.cs @@ -57,6 +57,7 @@ public Orcanode() DataplicityDescription = string.Empty; DataplicityName = string.Empty; DataplicitySerial = string.Empty; + OrcaHelloId = string.Empty; } #region persisted @@ -166,6 +167,11 @@ public Orcanode() /// public bool? OrcasoundVisible { get; set; } + /// + /// The "id" field from the OrcaHello hydrophones API. + /// + public string OrcaHelloId { get; set; } + #endregion persisted #region derived @@ -243,22 +249,8 @@ public OrcanodeOnlineStatus DataplicityConnectionStatus { } } -#if ORCAHELLO - public string OrcaHelloName - { - get - { - if (DisplayName == null) return string.Empty; - - // Any special cases here, since OrcaHello does not support - // node enumeration, nor does it use the same names as - // Dataplicity or Orcasound.net. - if (DisplayName == "Orcasound Lab") return "Haro Strait"; + public OrcanodeOnlineStatus OrcaHelloStatus => OrcaHelloId.IsNullOrEmpty() ? OrcanodeOnlineStatus.Absent : OrcanodeOnlineStatus.Online; - return DisplayName; - } - } -#endif public OrcanodeOnlineStatus OrcasoundStatus { get diff --git a/OrcanodeMonitor/Pages/Index.cshtml b/OrcanodeMonitor/Pages/Index.cshtml index 4fc65b3..c0728aa 100644 --- a/OrcanodeMonitor/Pages/Index.cshtml +++ b/OrcanodeMonitor/Pages/Index.cshtml @@ -23,6 +23,7 @@ SD Card Util. Orcasound S3 Stream + OrcaHello