From ffb326e126812e03058b36c4b02d6e10db3b760d Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Sun, 10 Sep 2023 15:40:33 +0200 Subject: [PATCH 01/13] Fixed warnings, updated libraries, had to rewrite a bit of Telegram due to new libraries. Also added prediction in the notification message. fixed deepstack logic to AIProcessor. --- SynoAI.Tests/packages.lock.json | 123 ++++++----- SynoAI/AIs/AI.cs | 18 +- .../AIProcessorAI.cs} | 46 +++- .../AIs/AIProcessor/AIProcessorPrediction.cs | 44 ++++ SynoAI/AIs/AIProcessor/AIProcessorRequest.cs | 16 ++ SynoAI/AIs/AIProcessor/AIProcessorResponse.cs | 17 ++ SynoAI/AIs/AIType.cs | 1 + SynoAI/AIs/DeepStack/DeepStackPrediction.cs | 19 -- SynoAI/AIs/DeepStack/DeepStackRequest.cs | 10 - SynoAI/AIs/DeepStack/DeepStackResponse.cs | 11 - SynoAI/App/IHttpClient.cs | 16 ++ SynoAI/Config.cs | 13 +- SynoAI/Controllers/CameraController.cs | 197 ++++++++++++++---- SynoAI/Controllers/HomeController.cs | 9 +- SynoAI/Notifiers/Discord/Discord.cs | 8 +- SynoAI/Notifiers/Email/EmailFactory.cs | 3 +- SynoAI/Notifiers/NotifierBase.cs | 35 +++- SynoAI/Notifiers/Pushbullet/Pushbullet.cs | 18 +- SynoAI/Notifiers/Pushover/Pushover.cs | 17 +- SynoAI/Notifiers/SynologyChat/SynologyChat.cs | 21 +- SynoAI/Notifiers/Telegram/Telegram.cs | 19 +- SynoAI/Notifiers/Webhook/Webhook.cs | 24 ++- SynoAI/Program.cs | 13 +- SynoAI/Services/AIService.cs | 4 +- SynoAI/Services/SnapshotManager.cs | 31 ++- SynoAI/Services/SynologyService.cs | 66 ++++-- SynoAI/SynoAI.csproj | 16 +- SynoAI/packages.lock.json | 123 ++++++----- 28 files changed, 640 insertions(+), 298 deletions(-) rename SynoAI/AIs/{DeepStack/DeepStackAI.cs => AIProcessor/AIProcessorAI.cs} (59%) create mode 100644 SynoAI/AIs/AIProcessor/AIProcessorPrediction.cs create mode 100644 SynoAI/AIs/AIProcessor/AIProcessorRequest.cs create mode 100644 SynoAI/AIs/AIProcessor/AIProcessorResponse.cs delete mode 100644 SynoAI/AIs/DeepStack/DeepStackPrediction.cs delete mode 100644 SynoAI/AIs/DeepStack/DeepStackRequest.cs delete mode 100644 SynoAI/AIs/DeepStack/DeepStackResponse.cs diff --git a/SynoAI.Tests/packages.lock.json b/SynoAI.Tests/packages.lock.json index c40f903..57bd1e9 100644 --- a/SynoAI.Tests/packages.lock.json +++ b/SynoAI.Tests/packages.lock.json @@ -35,12 +35,17 @@ "resolved": "2.4.5", "contentHash": "OwHamvBdUKgqsXfBzWiCW/O98BTx81UKzx2bieIOQI7CZFE5NEQZGi8PBQGIKawDW96xeRffiNf20SjfC0x9hw==" }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.2.1", + "contentHash": "A6Zr52zVqJKt18ZBsTnX0qhG0kwIQftVAjLmszmkiR/trSp8H+xj1gUOzk7XHwaKgyREMSV1v9XaKrBUeIOdvQ==" + }, "MailKit": { "type": "Transitive", - "resolved": "3.4.3", - "contentHash": "Iewef8mcE1B1LrVudxQjQ0LcriPPeTbxmWMoHQzFS+P6TpEY2eVDbdKdB0Qnbmqr/5w7WfK2mNWuoSX9pI470g==", + "resolved": "4.2.0", + "contentHash": "NXm66YkEHyLXSyH1Ga/dUS8SB0vYTlGESUluLULa7pG0/eK8c/R9JzMyH0KbKQsgpLGwbji9quAlrcUOL0OjPA==", "dependencies": { - "MimeKit": "3.4.3" + "MimeKit": "4.2.0" } }, "Microsoft.CodeCoverage": { @@ -88,8 +93,8 @@ }, "Microsoft.VisualStudio.Azure.Containers.Tools.Targets": { "type": "Transitive", - "resolved": "1.17.0", - "contentHash": "gfDtAL1WhkjbRdbZlt/ZeQYCbgRpNCZCGj+yeqHObsNFRDHjq8qZJOX9AyTxJpSRYMi9SJk7JDyAbbVYRgEhAA==" + "resolved": "1.19.5", + "contentHash": "Kaa1rBZdJFq5A0qgAcl6Bmk/UqLXTq9acEqxUlPEBA8oscmakLfkvuSXfG7Wa9t1/keaT85EuuDNgOo+Z9VYOQ==" }, "Microsoft.Win32.Primitives": { "type": "Transitive", @@ -108,19 +113,19 @@ }, "MimeKit": { "type": "Transitive", - "resolved": "3.4.3", - "contentHash": "7TSAcziEwk0bGWODpFTQASghXfYNBBa5VdM8KO4s5SBp5LYgIVXcQsdLBpPQ2XhZW74wfaX8RBUMs1GTMlLJcA==", + "resolved": "4.2.0", + "contentHash": "HlfWiJ6t40r8u/rCK2p/8dm1ILiWw4XHucm2HImDYIFS3uZe7IKZyaCDafEoZR7VG7AW1JQxNPQCAxmAnJfRvA==", "dependencies": { - "Portable.BouncyCastle": "1.9.0", + "BouncyCastle.Cryptography": "2.2.1", "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Security.Cryptography.Pkcs": "6.0.0", - "System.Text.Encoding.CodePages": "6.0.0" + "System.Security.Cryptography.Pkcs": "7.0.2", + "System.Text.Encoding.CodePages": "7.0.0" } }, "MQTTnet": { "type": "Transitive", - "resolved": "4.1.4.563", - "contentHash": "gO9segUcKyQJcjV7w7OOdoAIkec7cUN65vEhYutbdWcj4rbtz/oL/RDvQVVbameXc6ChkjKx7/HbO+R8ejAUZQ==" + "resolved": "4.3.1.873", + "contentHash": "5Btmzjv9TWQewlHL6QPB3/deTxAfHf0cR1ixehH/4311oKUpiYrgt1uQZFTbyBWjR7zVKv1U3+s4o3IPm/++Ww==" }, "NETStandard.Library": { "type": "Transitive", @@ -175,19 +180,14 @@ }, "Newtonsoft.Json": { "type": "Transitive", - "resolved": "13.0.2", - "contentHash": "R2pZ3B0UjeyHShm9vG+Tu0EBb2lC8b0dFzV9gVn50ofHXh9Smjk6kTn7A/FdAsC8B5cKib1OnGYOXxRBz5XQDg==" + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "NuGet.Frameworks": { "type": "Transitive", "resolved": "5.11.0", "contentHash": "eaiXkUjC4NPcquGWzAGMXjuxvLwc6XGKMptSyOGQeT0X70BUZObuybJFZLA0OfTdueLd3US23NBPTBb6iF3V1Q==" }, - "Portable.BouncyCastle": { - "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" - }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.0", @@ -297,62 +297,62 @@ }, "SkiaSharp": { "type": "Transitive", - "resolved": "2.88.3", - "contentHash": "GG8X3EdfwyBfwjl639UIiOVOKEdeoqDgYrz0P1MUCnefXt9cofN+AK8YB/v1+5cLMr03ieWCQdDmPqnFIzSxZw==", + "resolved": "2.88.5", + "contentHash": "43t9YEvcZtT+tuMN4hHH8rV8h7ttk1DRv5Tptxamy+bPuE8Up51+ME1Y1TXYAQf75686tHO488a0YKAnmJaSNg==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "2.88.3", - "SkiaSharp.NativeAssets.macOS": "2.88.3" + "SkiaSharp.NativeAssets.Win32": "2.88.5", + "SkiaSharp.NativeAssets.macOS": "2.88.5" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Transitive", - "resolved": "2.88.3", - "contentHash": "wz29evZVWRqN7WHfenFwQIgqtr8f5vHCutcl1XuhWrHTRZeaIBk7ngjhyHpjUMcQxtIEAdq34ZRvMQshsBYjqg==", + "resolved": "2.88.5", + "contentHash": "VMxHc9M9ENUJA7ZZ5q5HFsYZN7DjWtlokrP0pgGqVKX/SVk858s8LiO1yoa4PGde82JJh2y8tVjFpQ6zCcSa3w==", "dependencies": { - "SkiaSharp": "2.88.3" + "SkiaSharp": "2.88.5" } }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.88.3", - "contentHash": "CEbWAXMGFkPV3S1snBKK7jEG3+xud/9kmSAhu0BEUKKtlMdxx+Qal0U9bntQREM9QpqP5xLWZooodi8IlV8MEg==" + "resolved": "2.88.5", + "contentHash": "snAA6ghUHBeXSOEa/lbjYMUPDKdNh4YRjU+dBoU85EMDEPlAwtkM9gESTM8FKY67ozqX8tqLzad1RRDNistGsg==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.88.3", - "contentHash": "MU4ASL8VAbTv5vSw1PoiWjjjpjtGhWtFYuJnrN4sNHFCePb2ohQij9JhSdqLLxk7RpRtWPdV93fbA53Pt+J0yw==" + "resolved": "2.88.5", + "contentHash": "U0GuMsdtdbHjVmuyTPRbb+ifaTPYBIV8WvPFx81ZHwzfL2TVe8SQjum8AMXSZDDlf90rhxOC5slzaL33uoHUhA==" }, "Swashbuckle.AspNetCore": { "type": "Transitive", - "resolved": "6.4.0", - "contentHash": "eUBr4TW0up6oKDA5Xwkul289uqSMgY0xGN4pnbOIBqCcN9VKGGaPvHX3vWaG/hvocfGDP+MGzMA0bBBKz2fkmQ==", + "resolved": "6.5.0", + "contentHash": "FK05XokgjgwlCI6wCT+D4/abtQkL1X1/B9Oas6uIwHFmYrIO9WUD5aLC9IzMs9GnHfUXOtXZ2S43gN1mhs5+aA==", "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "6.0.5", - "Swashbuckle.AspNetCore.Swagger": "6.4.0", - "Swashbuckle.AspNetCore.SwaggerGen": "6.4.0", - "Swashbuckle.AspNetCore.SwaggerUI": "6.4.0" + "Swashbuckle.AspNetCore.Swagger": "6.5.0", + "Swashbuckle.AspNetCore.SwaggerGen": "6.5.0", + "Swashbuckle.AspNetCore.SwaggerUI": "6.5.0" } }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", - "resolved": "6.4.0", - "contentHash": "nl4SBgGM+cmthUcpwO/w1lUjevdDHAqRvfUoe4Xp/Uvuzt9mzGUwyFCqa3ODBAcZYBiFoKvrYwz0rabslJvSmQ==", + "resolved": "6.5.0", + "contentHash": "XWmCmqyFmoItXKFsQSwQbEAsjDKcxlNf1l+/Ki42hcb6LjKL8m5Db69OTvz5vLonMSRntYO1XLqz0OP+n3vKnA==", "dependencies": { "Microsoft.OpenApi": "1.2.3" } }, "Swashbuckle.AspNetCore.SwaggerGen": { "type": "Transitive", - "resolved": "6.4.0", - "contentHash": "lXhcUBVqKrPFAQF7e/ZeDfb5PMgE8n5t6L5B6/BQSpiwxgHzmBcx8Msu42zLYFTvR5PIqE9Q9lZvSQAcwCxJjw==", + "resolved": "6.5.0", + "contentHash": "Y/qW8Qdg9OEs7V013tt+94OdPxbRdbhcEbw4NiwGvf4YBcfhL/y7qp/Mjv/cENsQ2L3NqJ2AOu94weBy/h4KvA==", "dependencies": { - "Swashbuckle.AspNetCore.Swagger": "6.4.0" + "Swashbuckle.AspNetCore.Swagger": "6.5.0" } }, "Swashbuckle.AspNetCore.SwaggerUI": { "type": "Transitive", - "resolved": "6.4.0", - "contentHash": "1Hh3atb3pi8c+v7n4/3N80Jj8RvLOXgWxzix6w3OZhB7zBGRwsy7FWr4e3hwgPweSBpwfElqj4V4nkjYabH9nQ==" + "resolved": "6.5.0", + "contentHash": "OvbvxX+wL8skxTBttcBsVxdh73Fag4xwqEU2edh4JMn7Ws/xJHnY/JB1e9RoCb6XpDxUF3hD9A0Z1lEUx40Pfw==" }, "System.AppContext": { "type": "Transitive", @@ -465,8 +465,8 @@ }, "System.Formats.Asn1": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "T6fD00dQ3NTbPDy31m4eQUwKW84s03z0N2C8HpOklyeaDgaJPa/TexP4/SkORMSOwc7WhKifnA6Ya33AkzmafA==" + "resolved": "7.0.0", + "contentHash": "+nfpV0afLmvJW8+pLlHxRjz3oZJw4fkyU9MMEaMhCsHi/SN9bGF9q79ROubDiwTiCHezmK0uCWkPP7tGFP/4yg==" }, "System.Globalization": { "type": "Transitive", @@ -944,10 +944,10 @@ }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "elM3x+xSRhzQysiqo85SbidJJ2YbZlnvmh+53TuSZHsD7dNuuEWser+9EFtY+rYupBwkq2avc6ZCO3/6qACgmg==", + "resolved": "7.0.2", + "contentHash": "xhFNJOcQSWhpiVGLLBQYoxAltQSQVycMkwaX1z7I7oEdT9Wr0HzSM1yeAbfoHaERIYd5s6EpLSOLs2qMchSKlA==", "dependencies": { - "System.Formats.Asn1": "6.0.0" + "System.Formats.Asn1": "7.0.0" } }, "System.Security.Cryptography.Primitives": { @@ -1008,11 +1008,8 @@ }, "System.Text.Encoding.CodePages": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "7.0.0", + "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==" }, "System.Text.Encoding.Extensions": { "type": "Transitive", @@ -1115,10 +1112,10 @@ }, "Telegram.Bot": { "type": "Transitive", - "resolved": "18.0.0", - "contentHash": "BD0UchUXINymCGS+1O1tv2enCRyv+VbSJQAgfnueTZs3j7K4XXyJyW0CgyJleTrqB1oq1hS1ux6gBpi3Ajp+ZQ==", + "resolved": "19.0.0", + "contentHash": "Q16IOitgjGoaJOuqgKQy0FeF+hr/ncmlX2esrhCC7aiyhSX7roYEriWaGAHkQZR8QzbImjFfl4eQh2IxcnOrPg==", "dependencies": { - "Newtonsoft.Json": "12.0.2" + "Newtonsoft.Json": "13.0.1" } }, "xunit.abstractions": { @@ -1169,15 +1166,15 @@ "synoai": { "type": "Project", "dependencies": { - "MQTTnet": "[4.1.4.563, )", - "MailKit": "[3.4.3, )", - "Microsoft.VisualStudio.Azure.Containers.Tools.Targets": "[1.17.0, )", - "Newtonsoft.Json": "[13.0.2, )", - "SkiaSharp": "[2.88.3, )", - "SkiaSharp.NativeAssets.Linux": "[2.88.3, )", - "Swashbuckle.AspNetCore": "[6.4.0, )", + "MQTTnet": "[4.3.1.873, )", + "MailKit": "[4.2.0, )", + "Microsoft.VisualStudio.Azure.Containers.Tools.Targets": "[1.19.5, )", + "Newtonsoft.Json": "[13.0.3, )", + "SkiaSharp": "[2.88.5, )", + "SkiaSharp.NativeAssets.Linux": "[2.88.5, )", + "Swashbuckle.AspNetCore": "[6.5.0, )", "System.Drawing.Common": "[7.0.0, )", - "Telegram.Bot": "[18.0.0, )" + "Telegram.Bot": "[19.0.0, )" } } } diff --git a/SynoAI/AIs/AI.cs b/SynoAI/AIs/AI.cs index 3a14ec1..b99d002 100644 --- a/SynoAI/AIs/AI.cs +++ b/SynoAI/AIs/AI.cs @@ -2,8 +2,24 @@ namespace SynoAI.AIs { - internal abstract class AI + /// + /// Represents the base class for all AI implementations. + /// + public abstract class AI { + /// + /// Gets the type of the AI being used. + /// + /// The type of the AI. + public abstract AIType AIType { get; } + + /// + /// Processes the given image using the AI and returns the predictions. + /// + /// The logger to use for logging. + /// The camera from which the image was captured. + /// The image to be processed. + /// A list of predictions made by the AI. public abstract Task> Process(ILogger logger, Camera camera, byte[] image); } } diff --git a/SynoAI/AIs/DeepStack/DeepStackAI.cs b/SynoAI/AIs/AIProcessor/AIProcessorAI.cs similarity index 59% rename from SynoAI/AIs/DeepStack/DeepStackAI.cs rename to SynoAI/AIs/AIProcessor/AIProcessorAI.cs index 3c7c38d..c4e9e33 100644 --- a/SynoAI/AIs/DeepStack/DeepStackAI.cs +++ b/SynoAI/AIs/AIProcessor/AIProcessorAI.cs @@ -2,11 +2,13 @@ using SynoAI.App; using SynoAI.Models; using System.Diagnostics; +using System.Runtime.CompilerServices; -namespace SynoAI.AIs.DeepStack +namespace SynoAI.AIs.AIProcessor { - internal class DeepStackAI : AI + internal class AIProcessorAI : AI { + public override AIType AIType => Config.AI; public override async Task> Process(ILogger logger, Camera camera, byte[] image) { Stopwatch stopwatch = Stopwatch.StartNew(); @@ -19,7 +21,12 @@ public override async Task> Process(ILogger logger, Ca { new StringContent(minConfidence.ToString()), "min_confidence" } // From face detection example - using JSON with MinConfidence didn't always work }; - logger.LogDebug($"{camera.Name}: DeepStackAI: POSTing image with minimum confidence of {minConfidence} ({camera.Threshold}%) to {string.Join("/", Config.AIUrl, Config.AIPath)}."); + logger.LogDebug("{CameraName}: {AIType}: POSTing image with minimum confidence of {MinConfidence} ({CameraThreshold}%) to {Url}.", + camera.Name, + this.AIType, + minConfidence, + camera.Threshold, + string.Join("/", Config.AIUrl, Config.AIPath)); Uri uri = GetUri(Config.AIUrl, Config.AIPath); @@ -28,7 +35,7 @@ public override async Task> Process(ILogger logger, Ca HttpResponseMessage response = await Shared.HttpClient.PostAsync(uri, multipartContent); if (response.IsSuccessStatusCode) { - DeepStackResponse deepStackResponse = await GetResponse(logger, camera, response); + AIProcessorResponse deepStackResponse = await GetResponse(logger, camera, response, this.AIType); if (deepStackResponse.Success) { IEnumerable predictions = deepStackResponse.Predictions.Where(x => x.Confidence >= minConfidence).Select(x => new AIPrediction() @@ -42,22 +49,35 @@ public override async Task> Process(ILogger logger, Ca }).ToList(); stopwatch.Stop(); - logger.LogInformation($"{camera.Name}: DeepStackAI: Processed successfully ({stopwatch.ElapsedMilliseconds}ms)."); + logger.LogInformation("{CameraName}: {AIType}: Processed successfully ({ElapsedMilliseconds}ms).", + camera.Name, + this.AIType, + stopwatch.ElapsedMilliseconds); + return predictions; } else { - logger.LogWarning($"{camera.Name}: DeepStackAI: Failed with unknown error."); + logger.LogWarning("{cameraName}: {AIType}: Failed with unknown error.", + camera.Name, + this.AIType); } } else { - logger.LogWarning($"{camera.Name}: DeepStackAI: Failed to call API with HTTP status code '{response.StatusCode}'."); + logger.LogWarning("{cameraName}: {AIType}: Failed to call API with HTTP status code '{responseStatusCode}'.", + camera.Name, + this.AIType, + response.StatusCode); } } catch (HttpRequestException ex) { - logger.LogError($"{camera.Name}: DeepStackAI: Failed to call API error '{ex}'."); + logger.LogError("{camera.Name}: {AIType}: Failed to call API error '{ex}'.", + camera.Name, + this.AIType, + ex + ); } return null; @@ -81,13 +101,17 @@ protected static Uri GetUri(string basePath, string resourcePath) /// /// The message to parse. /// + /// /// A usable object. - private static async Task GetResponse(ILogger logger, Camera camera, HttpResponseMessage message) + private static async Task GetResponse(ILogger logger, Camera camera, HttpResponseMessage message, AIType aiType) { string content = await message.Content.ReadAsStringAsync(); - logger.LogDebug($"{camera.Name}: DeepStackAI: Responded with {content}."); + logger.LogDebug("{cameraName}: {AIType}: Responded with {content}.", + camera.Name, + aiType, + content); - return JsonConvert.DeserializeObject(content); + return JsonConvert.DeserializeObject(content); } } } diff --git a/SynoAI/AIs/AIProcessor/AIProcessorPrediction.cs b/SynoAI/AIs/AIProcessor/AIProcessorPrediction.cs new file mode 100644 index 0000000..436305f --- /dev/null +++ b/SynoAI/AIs/AIProcessor/AIProcessorPrediction.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; + +namespace SynoAI.AIs.AIProcessor +{ + /// + /// Represents a prediction made by AIProcessor AI. + /// + public class AIProcessorPrediction + { + /// + /// Gets or sets the confidence level of the prediction. + /// + public decimal Confidence { get; set; } + + /// + /// Gets or sets the label associated with the prediction. + /// + public string Label { get; set; } + + /// + /// Gets or sets the minimum X-coordinate of the bounding box for the prediction. + /// + [JsonProperty("x_min")] + public int MinX { get; set; } + + /// + /// Gets or sets the minimum Y-coordinate of the bounding box for the prediction. + /// + [JsonProperty("y_min")] + public int MinY { get; set; } + + /// + /// Gets or sets the maximum X-coordinate of the bounding box for the prediction. + /// + [JsonProperty("x_max")] + public int MaxX { get; set; } + + /// + /// Gets or sets the maximum Y-coordinate of the bounding box for the prediction. + /// + [JsonProperty("y_max")] + public int MaxY { get; set; } + } +} diff --git a/SynoAI/AIs/AIProcessor/AIProcessorRequest.cs b/SynoAI/AIs/AIProcessor/AIProcessorRequest.cs new file mode 100644 index 0000000..cdb10ce --- /dev/null +++ b/SynoAI/AIs/AIProcessor/AIProcessorRequest.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; + +namespace SynoAI.AIs.AIProcessor +{ + /// + /// Represents a request to DeepStackAI. + /// + public class AIProcessorRequest + { + /// + /// Gets or sets the minimum confidence for the request. + /// + [JsonProperty("min_confidence")] + public decimal MinConfidence { get; set; } + } +} diff --git a/SynoAI/AIs/AIProcessor/AIProcessorResponse.cs b/SynoAI/AIs/AIProcessor/AIProcessorResponse.cs new file mode 100644 index 0000000..2a12581 --- /dev/null +++ b/SynoAI/AIs/AIProcessor/AIProcessorResponse.cs @@ -0,0 +1,17 @@ +namespace SynoAI.AIs.AIProcessor +{ + /// + /// An object representing a response from DeepStack. + /// + public class AIProcessorResponse + { + /// + /// Gets or sets the succes + /// + public bool Success { get; set; } + /// + /// Gets or sets the collection of predictions made by DeepStack AI. + /// + public IEnumerable Predictions { get; set; } + } +} diff --git a/SynoAI/AIs/AIType.cs b/SynoAI/AIs/AIType.cs index 3d49d52..e0938fc 100644 --- a/SynoAI/AIs/AIType.cs +++ b/SynoAI/AIs/AIType.cs @@ -3,6 +3,7 @@ /// /// A list of support AI types. /// + /// public enum AIType { /// diff --git a/SynoAI/AIs/DeepStack/DeepStackPrediction.cs b/SynoAI/AIs/DeepStack/DeepStackPrediction.cs deleted file mode 100644 index e63860b..0000000 --- a/SynoAI/AIs/DeepStack/DeepStackPrediction.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Newtonsoft.Json; - -namespace SynoAI.AIs.DeepStack -{ - public class DeepStackPrediction - { - public decimal Confidence { get; set; } - public string Label { get; set; } - - [JsonProperty("x_min")] - public int MinX { get; set; } - [JsonProperty("y_min")] - public int MinY { get; set; } - [JsonProperty("x_max")] - public int MaxX { get; set; } - [JsonProperty("y_max")] - public int MaxY { get; set; } - } -} diff --git a/SynoAI/AIs/DeepStack/DeepStackRequest.cs b/SynoAI/AIs/DeepStack/DeepStackRequest.cs deleted file mode 100644 index 47be985..0000000 --- a/SynoAI/AIs/DeepStack/DeepStackRequest.cs +++ /dev/null @@ -1,10 +0,0 @@ -using Newtonsoft.Json; - -namespace SynoAI.AIs.DeepStack -{ - public class DeepStackRequest - { - [JsonProperty("min_confidence")] - public decimal MinConfidence { get; set; } - } -} diff --git a/SynoAI/AIs/DeepStack/DeepStackResponse.cs b/SynoAI/AIs/DeepStack/DeepStackResponse.cs deleted file mode 100644 index d969afa..0000000 --- a/SynoAI/AIs/DeepStack/DeepStackResponse.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace SynoAI.AIs.DeepStack -{ - /// - /// An object representing a response from DeepStack. - /// - public class DeepStackResponse - { - public bool Success { get; set; } - public IEnumerable Predictions { get; set; } - } -} diff --git a/SynoAI/App/IHttpClient.cs b/SynoAI/App/IHttpClient.cs index 320c639..2a076f9 100644 --- a/SynoAI/App/IHttpClient.cs +++ b/SynoAI/App/IHttpClient.cs @@ -1,8 +1,24 @@ namespace SynoAI.App { + /// + /// Represents a contract for HTTP client operations. + /// public interface IHttpClient { + /// + /// Sends a POST request to the specified Uri as an asynchronous operation. + /// + /// The Uri the request is sent to. + /// The HTTP content sent to the server. + /// The task object representing the asynchronous operation. Task PostAsync(string requestUri, HttpContent content); + + /// + /// Sends a POST request to the specified Uri as an asynchronous operation. + /// + /// The Uri the request is sent to. + /// The HTTP content sent to the server. + /// The task object representing the asynchronous operation. Task PostAsync(Uri requestUri, HttpContent content); } } diff --git a/SynoAI/Config.cs b/SynoAI/Config.cs index 31f9fac..5c5d54d 100644 --- a/SynoAI/Config.cs +++ b/SynoAI/Config.cs @@ -1,4 +1,8 @@ -using SkiaSharp; +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using SkiaSharp; using SynoAI.AIs; using SynoAI.Models; using SynoAI.Notifiers; @@ -226,7 +230,7 @@ public static void Generate(ILogger logger, IConfiguration configuration) DaysToKeepCaptures = configuration.GetValue("DaysToKeepCaptures", 0); IConfigurationSection aiSection = configuration.GetSection("AI"); - AI = aiSection.GetValue("Type", AIType.DeepStack); + AI = aiSection.GetValue("Type", AIType.CodeProjectAIServer); AIUrl = aiSection.GetValue("Url"); AIPath = aiSection.GetValue("Path","v1/vision/detection"); @@ -248,7 +252,7 @@ private static IEnumerable GenerateNotifiers(ILogger logger, IConfigu { logger.LogInformation("Processing notifier config."); - List notifiers = new List(); + List notifiers = new(); IConfigurationSection section = configuration.GetSection("Notifiers"); foreach (IConfigurationSection child in section.GetChildren()) @@ -257,7 +261,8 @@ private static IEnumerable GenerateNotifiers(ILogger logger, IConfigu if (!Enum.TryParse(type, out NotifierType notifier)) { - logger.LogError($"Notifier Type '{ type }' is not supported."); + logger.LogError("Notifier Type '{ type }' is not supported.", + type); throw new NotImplementedException(type); } diff --git a/SynoAI/Controllers/CameraController.cs b/SynoAI/Controllers/CameraController.cs index 892e89e..9637fe1 100644 --- a/SynoAI/Controllers/CameraController.cs +++ b/SynoAI/Controllers/CameraController.cs @@ -10,6 +10,7 @@ using System.Drawing; using System.Text; using SynoAI.Models.DTOs; +using static Org.BouncyCastle.Crypto.Engines.SM2Engine; namespace SynoAI.Controllers { @@ -31,6 +32,13 @@ public class CameraController : ControllerBase private static readonly ConcurrentDictionary _delayedCameraChecks = new(StringComparer.OrdinalIgnoreCase); private static readonly ConcurrentDictionary _enabledCameras = new(StringComparer.OrdinalIgnoreCase); + /// + /// Initializes a new instance of the class. + /// + /// The AI service. + /// The Synology service. + /// The logger. + /// The SignalR hub context. public CameraController(IAIService aiService, ISynologyService synologyService, ILogger logger, IHubContext hubContext) { @@ -58,7 +66,8 @@ public async void Get(string id) if (!enabled) { // The camera has been disabled, so don't process any requests - _logger.LogInformation($"{id}: Requests for this camera will not be processed as it is currently disabled."); + _logger.LogInformation("{id}: Requests for this camera will not be processed as it is currently disabled.", + id); return; } } @@ -67,7 +76,8 @@ public async void Get(string id) Camera camera = Config.Cameras.FirstOrDefault(x => x.Name.Equals(id, StringComparison.OrdinalIgnoreCase)); if (camera == null) { - _logger.LogError($"{id}: The camera was not found."); + _logger.LogError("{id}: The camera was not found.", + id); return; } @@ -77,7 +87,9 @@ public async void Get(string id) if (_delayedCameraChecks.TryGetValue(id, out DateTime ignoreUntil) && ignoreUntil >= DateTime.UtcNow) { // The camera is under a detection delay for the period specified, so ignore this request - _logger.LogInformation($"{id}: Requests for this camera will not be processed until {ignoreUntil}."); + _logger.LogInformation("{id}: Requests for this camera will not be processed until {ignoreUntil}.", + id, + ignoreUntil); return; } } @@ -88,14 +100,16 @@ public async void Get(string id) if (_runningCameraChecks.TryGetValue(id, out bool running) && running) { // The camera is already running, so ignore this request - _logger.LogInformation($"{id}: The request for this camera is already running and was ignored."); + _logger.LogInformation("{id}: The request for this camera is already running and was ignored.", + id); return; } else { // The camera isn't running, so mark it as running _runningCameraChecks.AddOrUpdate(id, true, (key, oldValue) => true); - _logger.LogDebug($"{id}: The camera is currently running; no other camera requests will be processed while this request is ongoing."); + _logger.LogDebug("{id}: The camera is currently running; no other camera requests will be processed while this request is ongoing.", + id); } } @@ -107,7 +121,8 @@ public async void Get(string id) // Wait if the camera has a wait if (camera.Wait > 0) { - _logger.LogInformation($"{id}: Waiting for {camera.Wait}ms before fetching snapshot."); + _logger.LogInformation("{id}: Waiting for {camera.Wait}ms before fetching snapshot.", + id); await Task.Delay(camera.Wait); } @@ -118,7 +133,11 @@ public async void Get(string id) for (int snapshotCount = 1; snapshotCount <= Config.MaxSnapshots; snapshotCount++) { // Take the snapshot from Surveillance Station - _logger.LogInformation($"{id}: Snapshot {snapshotCount} of {Config.MaxSnapshots} requested at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogInformation("{id}: Snapshot {snapshotCount} of {ConfigMaxSnapshots} requested at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + snapshotCount, + Config.MaxSnapshots, + overallStopwatch.ElapsedMilliseconds); byte[] snapshot = await GetSnapshot(id); if (snapshot == null) { @@ -126,7 +145,11 @@ public async void Get(string id) continue; } - _logger.LogInformation($"{id}: Snapshot {snapshotCount} of {Config.MaxSnapshots} received at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogInformation("{id}: Snapshot {snapshotCount} of {ConfigMaxSnapshots} received at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + snapshotCount, + Config.MaxSnapshots, + overallStopwatch.ElapsedMilliseconds); // See if the image needs to be rotated (or further processing in the future ?) before being analyzed by the AI snapshot = PreProcessSnapshot(camera, snapshot); @@ -140,7 +163,12 @@ public async void Get(string id) return; } - _logger.LogInformation($"{id}: Snapshot {snapshotCount} of {Config.MaxSnapshots} contains {predictions.Count()} objects at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogInformation("{id}: Snapshot {snapshotCount} of {ConfigMaxSnapshots} contains {predictionsCount} objects at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + snapshotCount, + Config.MaxSnapshots, + predictions.Count(), + overallStopwatch.ElapsedMilliseconds); int minSizeX = camera.GetMinSizeX(); int minSizeY = camera.GetMinSizeY(); @@ -153,7 +181,16 @@ public async void Get(string id) // Check if the prediction label is in the list of types the camera is looking for if (camera.Types != null && !camera.Types.Contains(prediction.Label, StringComparer.OrdinalIgnoreCase)) { - _logger.LogDebug($"{id}: Ignored '{prediction.Label}' ([{prediction.MinX},{prediction.MinY}],[{prediction.MaxX},{prediction.MaxY}]) as it's not in the valid type list ({string.Join(",", camera.Types)}) at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogDebug("{id}: Ignored '{predictionLabel}' ([{predictionMinX},{predictionMinY}],[{predictionMaxX},{predictionMaxY}]) as it's not in the valid type list ({camtypes}) at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + prediction.Label, + prediction.MinX, + prediction.MinY, + prediction.MaxX, + prediction.MaxY, + string.Join(",", camera.Types), + overallStopwatch.ElapsedMilliseconds); + } else { @@ -161,12 +198,32 @@ public async void Get(string id) if (prediction.SizeX < minSizeX || prediction.SizeY < minSizeY) { // The prediction is under the minimum specified size - _logger.LogDebug($"{id}: Ignored '{prediction.Label}' ([{prediction.MinX},{prediction.MinY}],[{prediction.MaxX},{prediction.MaxY}]) as it's under the minimum specified size ({minSizeX}x{minSizeY}) at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogDebug("{id}: Ignored '{predictionLabel}' ([{predictionMinX},{predictionMinY}],[{predictionMaxX},{predictionMaxY}]) as it's under the minimum specified size ({minSizeX}x{minSizeY}) at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + prediction.Label, + prediction.MinX, + prediction.MinY, + prediction.MaxX, + prediction.MaxY, + minSizeX, + minSizeY, + overallStopwatch.ElapsedMilliseconds); + } else if (prediction.SizeX > maxSizeX || prediction.SizeY > maxSizeY) { // The prediction has exceeded the maximum specified size - _logger.LogDebug($"{id}: Ignored '{prediction.Label}' ([{prediction.MinX},{prediction.MinY}],[{prediction.MaxX},{prediction.MaxY}]) as it exceeds the maximum specified size ({maxSizeX}x{maxSizeY}) at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogDebug("{id}: Ignored '{predictionLabel}' ([{predictionMinX},{predictionMinY}],[{predictionMaxX},{predictionMaxY}]) as it exceeds the maximum specified size ({maxSizeX}x{maxSizeY}) at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + prediction.Label, + prediction.MinX, + prediction.MinY, + prediction.MaxX, + prediction.MaxY, + maxSizeX, + maxSizeY, + overallStopwatch.ElapsedMilliseconds); + } else { @@ -174,7 +231,14 @@ public async void Get(string id) if (include) { validPredictions.Add(prediction); - _logger.LogDebug($"{id}: Found valid prediction '{prediction.Label}' ([{prediction.MinX},{prediction.MinY}],[{prediction.MaxX},{prediction.MaxY}]) at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogDebug("{id}: Found valid prediction '{predictionLabel}' ([{predictionMinX},{predictionMinY}],[{predictionMaxX},{predictionMaxY}]) at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + prediction.Label, + prediction.MinX, + prediction.MinY, + prediction.MaxX, + prediction.MaxY, + overallStopwatch.ElapsedMilliseconds); } } } @@ -185,7 +249,8 @@ public async void Get(string id) (Config.SaveOriginalSnapshot == SaveSnapshotMode.WithPredictions && predictions.Any()) || (Config.SaveOriginalSnapshot == SaveSnapshotMode.WithValidPredictions && validPredictions.Any())) { - _logger.LogInformation($"{id}: Saving original image"); + _logger.LogInformation("{id}: Saving original image", + id); SnapshotManager.SaveOriginalImage(_logger, camera, snapshot); } @@ -205,7 +270,11 @@ public async void Get(string id) // Inform eventual web users about this new Snapshot, for the "realtime" option thru Web await _hubContext.Clients.All.SendAsync("ReceiveSnapshot", camera.Name, processedImage.FileName); - _logger.LogInformation($"{id}: Valid object found in snapshot {snapshotCount} of {Config.MaxSnapshots} at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogInformation("{id}: Valid object found in snapshot {snapshotCount} of {ConfigMaxSnapshots} at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + snapshotCount, + Config.MaxSnapshots, + overallStopwatch.ElapsedMilliseconds); // Extend the delay until the next motion detection will be run if a delay after success is specified int successDelay = camera.GetDelayAfterSuccess(); @@ -215,24 +284,34 @@ public async void Get(string id) else if (predictions.Any()) { // We got predictions back from the AI, but nothing that should trigger an alert - _logger.LogInformation($"{id}: No valid objects at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); - } + _logger.LogInformation("{id}: No valid objects at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + overallStopwatch.ElapsedMilliseconds); + } else { // We didn't get any predictions whatsoever from the AI - _logger.LogInformation($"{id}: Nothing detected by the AI at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogInformation("{id}: Nothing detected by the AI at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + overallStopwatch.ElapsedMilliseconds); + - StringBuilder nothingFoundOutput = new StringBuilder($"{id}: No objects "); + StringBuilder nothingFoundOutput = new($"{id}: No objects "); if (camera.Types != null && camera.Types.Any()) { nothingFoundOutput.Append($"in the specified list ({string.Join(", ", camera.Types)}) "); } nothingFoundOutput.Append($"were detected by the AI exceeding the confidence level ({camera.Threshold}%) and/or minimum size ({minSizeX}x{minSizeY} and/or maximum size ({maxSizeX},{maxSizeY}))"); - _logger.LogDebug(nothingFoundOutput.ToString()); + _logger.LogDebug("{Output}", + nothingFoundOutput.ToString()); + } - _logger.LogInformation($"{id}: Finished ({overallStopwatch.ElapsedMilliseconds}ms)."); + _logger.LogInformation("{id}: Finished ({overallStopwatchElapsedMilliseconds}ms).", + id, + overallStopwatch.ElapsedMilliseconds); + } // Add the delay (if any) @@ -244,7 +323,8 @@ public async void Get(string id) // Ensure the camera is unflagged as running lock (_runningCameraChecks) { - _logger.LogDebug($"{id}: Removing running camera block."); + _logger.LogDebug("{id}: Removing running camera block.", + id); _runningCameraChecks.Remove(id, out _); } } @@ -252,7 +332,10 @@ public async void Get(string id) [HttpPost] [Route("{id}")] + + public void Post(string id, [FromBody]CameraOptionsDto options) + { if (options.HasChanged(x=> x.Enabled)) { @@ -285,7 +368,20 @@ private bool ShouldIncludePrediction(string id, Camera camera, Stopwatch overall if (exclude) { // The prediction boundary is contained within or intersects and exclusion zone, so ignore it ; - _logger.LogDebug($"{id}: Ignored matching '{prediction.Label}' ([{prediction.MinX},{prediction.MinY}],[{prediction.MaxX},{prediction.MaxY}]) as it fell within the exclusion zone ([{exclusion.Start.X},{exclusion.Start.Y}],[{exclusion.End.X},{exclusion.End.Y}]) with exclusion mode '{exclusion.Mode}' at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + _logger.LogDebug("{id}: Ignored matching '{predictionLabel}' ([{predictionMinX},{predictionMinY}],[{predictionMaxX},{predictionMaxY}]) as it fell within the exclusion zone ([{exclusionStartX},{exclusionStartY}],[{exclusionEndX},{exclusionEndY}]) with exclusion mode '{exclusionMode}' at EVENT TIME {overallStopwatchElapsedMilliseconds}ms.", + id, + prediction.Label, + prediction.MinX, + prediction.MinY, + prediction.MaxX, + prediction.MaxY, + exclusion.Start.X, + exclusion.Start.Y, + exclusion.End.X, + exclusion.End.Y, + exclusion.Mode, + overallStopwatch.ElapsedMilliseconds + ); return false; } } @@ -310,7 +406,9 @@ private void AddCameraDelay(string id, int delay) { DateTime ignoreUntil = DateTime.UtcNow.AddMilliseconds(delay); _delayedCameraChecks.AddOrUpdate(id, ignoreUntil, (key, oldValue) => ignoreUntil); - _logger.LogDebug($"{id}: Added delay of {delay} until the next request will be processed."); + _logger.LogDebug("{id}: Added delay of {delay} until the next request will be processed.", + id, + delay); } } @@ -326,7 +424,8 @@ private void CleanupOldImages() if (Config.DaysToKeepCaptures > 0 && !_cleanupOldImagesRunning) { - _logger.LogInformation($"Captures Clean Up: Cleaning up images older than {Config.DaysToKeepCaptures} day(s)."); + _logger.LogInformation("Captures Clean Up: Cleaning up images older than {ConfigDaysToKeepCaptures} day(s).", + Config.DaysToKeepCaptures); Task.Run(() => { lock (_cleanUpOldImagesLock) @@ -340,9 +439,12 @@ private void CleanupOldImages() double age = (DateTime.Now - file.CreationTime).TotalDays; if (age > Config.DaysToKeepCaptures) { - _logger.LogInformation($"Captures Clean Up: {file.FullName} is {age} day(s) old and will be deleted."); + _logger.LogInformation("Captures Clean Up: {fileFullName} is {age} day(s) old and will be deleted.", + file.FullName, + age); System.IO.File.Delete(file.FullName); - _logger.LogInformation($"Captures Clean Up: {file.FullName} deleted."); + _logger.LogInformation("Captures Clean Up: {fileFullName} deleted.", + file.FullName); } } _cleanupOldImagesRunning = false; @@ -368,15 +470,17 @@ private byte[] PreProcessSnapshot(Camera camera, byte[] snapshot) // Load the bitmap & rotate the image SKBitmap bitmap = SKBitmap.Decode(snapshot); - _logger.LogInformation($"{camera.Name}: Rotating image {camera.Rotate} degrees."); + _logger.LogInformation("{cameraName}: Rotating image {cameraRotate} degrees.", + camera.Name, + camera.Rotate); bitmap = Rotate(bitmap, camera.Rotate); - using (SKPixmap pixmap = bitmap.PeekPixels()) - using (SKData data = pixmap.Encode(SKEncodedImageFormat.Jpeg, 100)) - { - _logger.LogInformation($"{camera.Name}: Image preprocessing complete ({stopwatch.ElapsedMilliseconds}ms)."); - return data.ToArray(); - } + using SKPixmap pixmap = bitmap.PeekPixels(); + using SKData data = pixmap.Encode(SKEncodedImageFormat.Jpeg, 100); + _logger.LogInformation("{cameraName}: Image preprocessing complete ({stopwatchElapsedMilliseconds}ms).", + camera.Name, + stopwatch.ElapsedMilliseconds); + return data.ToArray(); } else { @@ -439,7 +543,9 @@ private async Task SendNotifications(Camera camera, Notification notification) await Task.WhenAll(tasks); stopwatch.Stop(); - _logger.LogInformation($"{camera.Name}: Notifications sent ({stopwatch.ElapsedMilliseconds}ms)."); + _logger.LogInformation("{camera.Name}: Notifications sent ({stopwatchElapsedMilliseconds}ms).", + camera.Name, + stopwatch.ElapsedMilliseconds); } /// @@ -455,11 +561,14 @@ private async Task GetSnapshot(string cameraName) if (imageBytes == null) { - _logger.LogError($"{cameraName}: Failed to get snapshot."); + _logger.LogError("{cameraName}: Failed to get snapshot.", + cameraName); } else { - _logger.LogInformation($"{cameraName}: Snapshot received in {stopwatch.ElapsedMilliseconds}ms."); + _logger.LogInformation("{cameraName}: Snapshot received in {stopwatchElapsedMilliseconds}ms.", + cameraName, + stopwatch.ElapsedMilliseconds); } return imageBytes; } @@ -475,13 +584,23 @@ private async Task> GetAIPredications(Camera camera, b IEnumerable predictions = await _aiService.ProcessAsync(camera, imageBytes); if (predictions == null) { - _logger.LogError($"{camera}: Failed to get get predictions."); + _logger.LogError("{camera}: Failed to get get predictions.", + camera); return null; } foreach (AIPrediction prediction in predictions) { - _logger.LogInformation($"AI Detected '{camera}': {prediction.Label} ({prediction.Confidence}%) [Size: {prediction.SizeX}x{prediction.SizeY}] [Start: {prediction.MinX},{prediction.MinY} | End: {prediction.MaxX},{prediction.MaxY}]"); + _logger.LogInformation("AI Detected '{camera}': {prediction.Label} ({prediction.Confidence}%) [Size: {prediction.SizeX}x{prediction.SizeY}] [Start: {prediction.MinX},{prediction.MinY} | End: {prediction.MaxX},{prediction.MaxY}]", + camera, + prediction.Label, + prediction.Confidence, + prediction.SizeX, + prediction.SizeY, + prediction.MinX, + prediction.MinY, + prediction.MaxX, + prediction.MaxY); } return predictions; diff --git a/SynoAI/Controllers/HomeController.cs b/SynoAI/Controllers/HomeController.cs index 31a9007..9b1f539 100644 --- a/SynoAI/Controllers/HomeController.cs +++ b/SynoAI/Controllers/HomeController.cs @@ -6,6 +6,9 @@ namespace SynoAI.Controllers { + /// + /// Controller for handling home-related HTTP requests. + /// public class HomeController : Controller { static readonly string[] byteSizes = { "bytes", "Kb", "Mb", "Gb", "Tb" }; @@ -128,7 +131,7 @@ public ActionResult Snapshot(string cameraName, string filename, int width = 0) /// public static GraphData GetData(string cameraName, DateTime date, bool GraphHour = false) { - GraphData data = new GraphData(); + GraphData data = new(); string directory = Path.Combine(Constants.DIRECTORY_CAPTURES, cameraName); if (Directory.Exists(directory)) @@ -218,7 +221,7 @@ public static GraphData GetData(string cameraName, DateTime date, bool GraphHour /// public static List GetSnapshots(string cameraName, DateTime date) { - List files = new List(); + List files = new(); string directory = Path.Combine(Constants.DIRECTORY_CAPTURES, cameraName); if (Directory.Exists(directory)) @@ -247,9 +250,9 @@ public static List GetSnapshots(string cameraName, DateTime date) /// private static int GetObjects(string filename) { - int objects = 0; string name = Path.GetFileNameWithoutExtension(filename); int index = name.IndexOf("-"); + int objects; if (index != -1) { //try to extract the number of valid objects predicted inside this snapshot diff --git a/SynoAI/Notifiers/Discord/Discord.cs b/SynoAI/Notifiers/Discord/Discord.cs index 5624251..7a0dd4e 100644 --- a/SynoAI/Notifiers/Discord/Discord.cs +++ b/SynoAI/Notifiers/Discord/Discord.cs @@ -16,7 +16,8 @@ public override async Task SendAsync(Camera camera, Notification notification, I ProcessedImage processedImage = notification.ProcessedImage; // Discord seems to require double escaped newlines - var message = GetMessage(camera, notification.FoundTypes); + //var message = GetMessage(camera, notification.FoundTypes); + var message = GetMessage(camera, notification.FoundTypes, new List()); message = message.Replace("\n", "\\n"); formData.Add(new StringContent($"{{\"content\":\"{message}\"}}"), "payload_json"); @@ -30,7 +31,10 @@ public override async Task SendAsync(Camera camera, Notification notification, I else { string error = await responseMessage.Content.ReadAsStringAsync(); - logger.LogError($"{camera.Name}: Discord: The end point responded with HTTP status code '{responseMessage.StatusCode}' and error '{error}'."); + logger.LogError("{cameraName}: Discord: The end point responded with HTTP status code '{responseMessageStatusCode}' and error '{error}'.", + camera.Name, + responseMessage.StatusCode, + error); } } } diff --git a/SynoAI/Notifiers/Email/EmailFactory.cs b/SynoAI/Notifiers/Email/EmailFactory.cs index 55370e4..dab1dd2 100644 --- a/SynoAI/Notifiers/Email/EmailFactory.cs +++ b/SynoAI/Notifiers/Email/EmailFactory.cs @@ -51,7 +51,8 @@ private static SecureSocketOptions GetSecureSocketOptions(ILogger logger, IConfi case "STARTTLSWHENAVAILABLE": return SecureSocketOptions.StartTlsWhenAvailable; default: - logger.LogError($"The email encryption type '{options}' is not supported.", options); + logger.LogError("The email encryption type '{options}' is not supported." + , options); throw new NotSupportedException($"The email SecureSocketOptions type '{options}' is not supported."); } } diff --git a/SynoAI/Notifiers/NotifierBase.cs b/SynoAI/Notifiers/NotifierBase.cs index 510ca78..2b84448 100644 --- a/SynoAI/Notifiers/NotifierBase.cs +++ b/SynoAI/Notifiers/NotifierBase.cs @@ -16,7 +16,8 @@ internal abstract class NotifierBase : INotifier public virtual Task CleanupAsync(ILogger logger) { return Task.CompletedTask; } - protected static string GetMessage(Camera camera, IEnumerable foundTypes, string errorMessage = null) + protected static string GetMessage(Camera camera, IEnumerable foundTypes, List predictions, string errorMessage = null) + { string result ; if (Config.AlternativeLabelling && Config.DrawMode == DrawMode.Matches) @@ -38,17 +39,34 @@ protected static string GetMessage(Camera camera, IEnumerable foundTypes { // Just one object detected result = $"{camera.Name}: {foundTypes.First()}"; - } + } + // Include prediction confidence + foreach (var prediction in predictions) + { + result += $"\n{prediction.Label}: {prediction.Confidence}%"; + } + } else { // Standard (old) labelling result = $"Motion detected on {camera.Name}\n\nDetected {foundTypes.Count()} objects:\n{String.Join("\n", foundTypes.Select(x => x).ToArray())}"; } + // Include prediction confidence + foreach (var prediction in predictions) + { + result += $"\n{prediction.Label}: {prediction.Confidence}%"; + } + if (!string.IsNullOrWhiteSpace(errorMessage)){ result += $"\nAn error occurred during the creation of the notification: {errorMessage}"; } + if (predictions != null && predictions.Any()) + { + result += "\nPredictions:\n"; + result += string.Join("\n", predictions.Select(prediction => $"{prediction.Label} ({prediction.Confidence}%)")); + } return result; } @@ -75,8 +93,16 @@ protected static string GenerateJSON(Camera camera, Notification notification, b jsonObject.camera = camera.Name; jsonObject.foundTypes = notification.FoundTypes; - jsonObject.predictions = notification.ValidPredictions; - jsonObject.message = GetMessage(camera, notification.FoundTypes); + + List validPredictions = notification.ValidPredictions.ToList(); + jsonObject.predictions = validPredictions.Select(prediction => new + { + Confidence = prediction.Confidence, + Label = prediction.Label, + // Add other properties as needed + }).ToList(); + + jsonObject.message = GetMessage(camera, notification.FoundTypes, validPredictions); if (sendImage) { @@ -92,6 +118,7 @@ protected static string GenerateJSON(Camera camera, Notification notification, b return JsonConvert.SerializeObject(jsonObject); } + /// /// Returns FileStream data as a base64-encoded string /// diff --git a/SynoAI/Notifiers/Pushbullet/Pushbullet.cs b/SynoAI/Notifiers/Pushbullet/Pushbullet.cs index 5bc56ea..d3a2973 100644 --- a/SynoAI/Notifiers/Pushbullet/Pushbullet.cs +++ b/SynoAI/Notifiers/Pushbullet/Pushbullet.cs @@ -67,7 +67,9 @@ public override async Task SendAsync(Camera camera, Notification notification, I { PushbulletErrorResponse error = await GetResponse(uploadFileResponse); uploadError = $"Pushbullet error uploading file ({error.Error})"; - logger.LogError($"{camera.Name}: {uploadError}"); + logger.LogError("{camera.Name}: {uploadError}", + camera.Name, + uploadError); } // The file was uploaded successfully, so we can now send the message @@ -75,7 +77,8 @@ public override async Task SendAsync(Camera camera, Notification notification, I { Type = uploadSuccess ? "file" : "note", Title = $"{camera.Name}: Movement Detected", - Body = GetMessage(camera, notification.FoundTypes, errorMessage: uploadError), + //Body = GetMessage(camera, notification.FoundTypes, errorMessage: uploadError), + Body = GetMessage(camera, notification.FoundTypes, new List(), errorMessage: uploadError), FileName = uploadSuccess ? uploadRequestResult.FileName : null, FileUrl = uploadSuccess ? uploadRequestResult.FileUrl : null, FileType = uploadSuccess ? uploadRequestResult.FileType : null @@ -87,19 +90,24 @@ public override async Task SendAsync(Camera camera, Notification notification, I HttpResponseMessage pushResponse = await Shared.HttpClient.PostAsync(new Uri(URI_PUSHES), push); if (pushResponse.IsSuccessStatusCode) { - logger.LogInformation($"{camera.Name}: Pushbullet notification sent successfully"); + logger.LogInformation("{camera.Name}: Pushbullet notification sent successfully", + camera.Name); } else { PushbulletErrorResponse error = await GetResponse(pushResponse); - logger.LogError($"{camera.Name}: Pushbullet error sending push ({error.Error})"); + logger.LogError("{camera.Name}: Pushbullet error sending push ({error.Error})", + camera.Name, + error.Error); } } } else { PushbulletErrorResponse error = await GetResponse(requestResponse); - logger.LogError($"{camera.Name}: Pushbullet error requesting upload ({error.Error})"); + logger.LogError("{cameraName}: Pushbullet error requesting upload ({errorError})", + camera.Name, + error.Error); } } diff --git a/SynoAI/Notifiers/Pushover/Pushover.cs b/SynoAI/Notifiers/Pushover/Pushover.cs index 5c89ea4..df8b766 100644 --- a/SynoAI/Notifiers/Pushover/Pushover.cs +++ b/SynoAI/Notifiers/Pushover/Pushover.cs @@ -52,9 +52,11 @@ public override async Task SendAsync(Camera camera, Notification notification, I } // Build the form message - logger.LogInformation($"{camera.Name}: Pushover: Building message"); + logger.LogInformation("{cameraName}: Pushover: Building message", + camera.Name); - string message = GetMessage(camera, notification.FoundTypes); + //string message = GetMessage(camera, notification.FoundTypes); + string message = GetMessage(camera, notification.FoundTypes, new List()); string device = Devices == null || !Devices.Any() ? String.Empty : string.Join(',', Devices); string title = $"{camera.Name}: Movement Detected"; @@ -84,16 +86,21 @@ public override async Task SendAsync(Camera camera, Notification notification, I param.Headers.ContentType = null; } - logger.LogInformation($"{camera.Name}: Pushover: Sending message"); + logger.LogInformation("{cameraName}: Pushover: Sending message", + camera.Name); HttpResponseMessage responseMessage = await Shared.HttpClient.PostAsync(URI_MESSAGE, form); if (responseMessage.IsSuccessStatusCode) { - logger.LogInformation($"{camera.Name}: Pushover: Notification sent successfully"); + logger.LogInformation("{cameraName}: Pushover: Notification sent successfully", + camera.Name); } else { string error = await responseMessage.Content.ReadAsStringAsync(); - logger.LogError($"{camera.Name}: Pushover: The end point responded with HTTP status code '{responseMessage.StatusCode}' and error '{error}'."); + logger.LogError("{cameraName}: Pushover: The end point responded with HTTP status code '{responseMessageStatusCode}' and error '{error}'.", + camera.Name, + responseMessage.StatusCode, + error); } } } diff --git a/SynoAI/Notifiers/SynologyChat/SynologyChat.cs b/SynoAI/Notifiers/SynologyChat/SynologyChat.cs index f692ab1..b461d1b 100644 --- a/SynoAI/Notifiers/SynologyChat/SynologyChat.cs +++ b/SynoAI/Notifiers/SynologyChat/SynologyChat.cs @@ -21,11 +21,13 @@ internal class SynologyChat : NotifierBase /// A logger. public override async Task SendAsync(Camera camera, Notification notification, ILogger logger) { - logger.LogInformation($"{camera.Name}: SynologyChat: Processing"); + logger.LogInformation("{cameraName}: SynologyChat: Processing", + camera.Name); using (HttpClient client = new()) { IEnumerable foundTypes = notification.FoundTypes; - string message = GetMessage(camera, foundTypes); + string message = GetMessage(camera, foundTypes, new List()); + var request = new { @@ -44,7 +46,8 @@ public override async Task SendAsync(Camera camera, Notification notification, I content.Headers.Clear(); content.Headers.Add("Content-Type", "application/x-www-form-urlencoded"); - logger.LogInformation($"{camera.Name}: SynologyChat: POSTing message."); + logger.LogInformation("{camera.Name}: SynologyChat: POSTing message.", + camera.Name); HttpResponseMessage response = await client.PostAsync(Url, content); if (response.IsSuccessStatusCode) @@ -54,16 +57,22 @@ public override async Task SendAsync(Camera camera, Notification notification, I SynologyChatResponse actualResponse = JsonConvert.DeserializeObject(responseString); if (actualResponse.Success) { - logger.LogInformation($"{camera.Name}: SynologyChat: Success."); + logger.LogInformation("{cameraName}: SynologyChat: Success.", + camera.Name); } else { - logger.LogInformation($"{camera.Name}: SynologyChat: Failed with error '{actualResponse.Error.Code}': {actualResponse.Error.Errors}."); + logger.LogInformation("{cameraName}: SynologyChat: Failed with error '{actualResponseErrorCode}': {actualResponseErrorErrors}.", + camera.Name, + actualResponse.Error.Code, + actualResponse.Error.Errors); } } else { - logger.LogWarning($"{camera.Name}: SynologyChat: The end point responded with HTTP status code '{response.StatusCode}'."); + logger.LogWarning("{cameraName}: SynologyChat: The end point responded with HTTP status code '{responseStatusCode}'.", + camera.Name, + response.StatusCode); } } } diff --git a/SynoAI/Notifiers/Telegram/Telegram.cs b/SynoAI/Notifiers/Telegram/Telegram.cs index 8be2a34..9f3e94a 100644 --- a/SynoAI/Notifiers/Telegram/Telegram.cs +++ b/SynoAI/Notifiers/Telegram/Telegram.cs @@ -1,5 +1,8 @@ +using Microsoft.AspNetCore.Components.Forms; using SynoAI.Models; using Telegram.Bot; +using Telegram.Bot.Types; +using System.Net.Http; namespace SynoAI.Notifiers.Telegram { @@ -40,20 +43,24 @@ public override async Task SendAsync(Camera camera, Notification notification, I { TelegramBotClient bot = new(Token); - string message = GetMessage(camera, foundTypes); + //string message = GetMessage(camera, foundTypes); + string message = GetMessage(camera, foundTypes, new List()); + if (string.IsNullOrWhiteSpace(PhotoBaseURL)) { // The photo base URL hasn't been specified, which means we need to send the file ourselves - using (FileStream fileStream = processedImage.GetReadonlyStream()) - { - await bot.SendPhotoAsync(ChatID, fileStream, message); - } + using FileStream fileStream = processedImage.GetReadonlyStream(); + var inputFile = new InputFileStream(fileStream, processedImage.FileName); + await bot.SendPhotoAsync(chatId: ChatID, photo: inputFile, caption: message); // TODO - Add a config to disable the sending of the image? } else { string photoUrl = $"{PhotoBaseURL}/{camera.Name}/{processedImage.FileName}"; - await bot.SendPhotoAsync(ChatID, photoUrl, message); + //api requires a download of the file + using HttpClient httpClient = new(); + using Stream photoStream = await httpClient.GetStreamAsync(photoUrl); + await bot.SendPhotoAsync(chatId: ChatID, photo: new InputFileStream(photoStream, processedImage.FileName), caption: message); } logger.LogInformation("{cameraName}: Telegram notification sent successfully", cameraName); diff --git a/SynoAI/Notifiers/Webhook/Webhook.cs b/SynoAI/Notifiers/Webhook/Webhook.cs index ebe5235..79e3fbc 100644 --- a/SynoAI/Notifiers/Webhook/Webhook.cs +++ b/SynoAI/Notifiers/Webhook/Webhook.cs @@ -54,14 +54,15 @@ internal class Webhook : NotifierBase /// A logger. public override async Task SendAsync(Camera camera, Notification notification, ILogger logger) { - logger.LogInformation($"{camera.Name}: Webhook: Processing"); + logger.LogInformation("{cameraName}: Webhook: Processing", + camera.Name); using (HttpClient client = new()) { FileStream fileStream = null; client.DefaultRequestHeaders.Authorization = GetAuthenticationHeader(); IEnumerable foundTypes = notification.FoundTypes; - string message = GetMessage(camera, foundTypes); + string message = GetMessage(camera, foundTypes, new List()); HttpContent content; if (SendImage) @@ -103,7 +104,9 @@ public override async Task SendAsync(Camera camera, Notification notification, I content = new StringContent(GenerateJSON(camera, notification, false), null, "application/json"); } - logger.LogInformation($"{camera.Name}: Webhook: Calling {Method}."); + logger.LogInformation("{cameraName}: Webhook: Calling {Method}.", + camera.Name, + Method); HttpResponseMessage response; switch (Method) @@ -124,18 +127,25 @@ public override async Task SendAsync(Camera camera, Notification notification, I response = await client.PutAsync(Url, content); break; default: - logger.LogError($"{camera.Name}: Webhook: The method type '{Method}' is not supported."); + logger.LogError("{cameraName}: Webhook: The method type '{Method}' is not supported.", + camera.Name, + Method); return; } if (response.IsSuccessStatusCode) { - logger.LogInformation($"{camera.Name}: Webhook: Success."); - logger.LogDebug($"{camera.Name}: Webhook: Success with HTTP status code '{response.StatusCode}'."); + logger.LogInformation("{cameraName}: Webhook: Success.", + camera.Name); + logger.LogDebug("{cameraName}: Webhook: Success with HTTP status code '{responseStatusCode}'.", + camera.Name, + response.StatusCode); } else { - logger.LogWarning($"{camera.Name}: Webhook: The end point responded with HTTP status code '{response.StatusCode}'."); + logger.LogWarning("{cameraName}: Webhook: The end point responded with HTTP status code '{responseStatusCode}'.", + camera.Name, + response.StatusCode); } if (fileStream != null) diff --git a/SynoAI/Program.cs b/SynoAI/Program.cs index bedf57b..deda1fd 100644 --- a/SynoAI/Program.cs +++ b/SynoAI/Program.cs @@ -1,12 +1,23 @@ namespace SynoAI { + /// + /// The main program class responsible for starting the SynoAI application. + /// public class Program { + /// + /// The entry point of the application. + /// + /// The command-line arguments. public static void Main(string[] args) { CreateHostBuilder(args).Build().Run(); } - + /// + /// Main method to create and configure the host for the SynoAI application. + /// + /// Command-line arguments. + /// An that configures the host for the application. public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => diff --git a/SynoAI/Services/AIService.cs b/SynoAI/Services/AIService.cs index 7816c58..1d64c15 100644 --- a/SynoAI/Services/AIService.cs +++ b/SynoAI/Services/AIService.cs @@ -1,5 +1,5 @@ using SynoAI.AIs; -using SynoAI.AIs.DeepStack; +using SynoAI.AIs.AIProcessor; using SynoAI.Models; namespace SynoAI.Services @@ -25,7 +25,7 @@ private static AI GetAI() { case AIType.DeepStack: case AIType.CodeProjectAIServer: // Works the same as DeepStack - return new DeepStackAI(); + return new AIProcessorAI(); default: throw new NotImplementedException(Config.AI.ToString()); } diff --git a/SynoAI/Services/SnapshotManager.cs b/SynoAI/Services/SnapshotManager.cs index 08b1165..a25b549 100644 --- a/SynoAI/Services/SnapshotManager.cs +++ b/SynoAI/Services/SnapshotManager.cs @@ -25,7 +25,8 @@ public static ProcessedImage DressImage(Camera camera, byte[] snapshot, IEnumera // Draw the exclusion zones if enabled if (Config.DrawExclusions && camera.Exclusions != null) { - logger.LogInformation($"{camera.Name}: Drawing exclusion zones."); + logger.LogInformation("{camera.Name}: Drawing exclusion zones.", + camera.Name); using (SKCanvas canvas = new SKCanvas(image)) { @@ -46,12 +47,14 @@ public static ProcessedImage DressImage(Camera camera, byte[] snapshot, IEnumera // Don't process the drawing if the drawing mode is off if (Config.DrawMode == DrawMode.Off) { - logger.LogInformation($"{camera.Name}: Draw mode is Off. Skipping image boundaries."); + logger.LogInformation("{camera.Name}: Draw mode is Off. Skipping image boundaries.", + camera.Name); } else { // Draw the predictions - logger.LogInformation($"{camera.Name}: Dressing image with boundaries."); + logger.LogInformation("{camera.Name}: Dressing image with boundaries.", + camera.Name); using (SKCanvas canvas = new SKCanvas(image)) { int counter = 1; //used for assigning a reference number on each prediction if AlternativeLabelling is true @@ -137,7 +140,9 @@ public static ProcessedImage DressImage(Camera camera, byte[] snapshot, IEnumera } stopwatch.Stop(); - logger.LogInformation($"{camera.Name}: Finished dressing image boundaries ({stopwatch.ElapsedMilliseconds}ms)."); + logger.LogInformation("{camera.Name}: Finished dressing image boundaries ({stopwatchElapsedMilliseconds}ms).", + camera.Name, + stopwatch.ElapsedMilliseconds); // Save the image, including the amount of valid predictions as suffix. String filePath = SaveImage(logger,camera, image, validPredictions.Count().ToString()); @@ -174,7 +179,9 @@ private static string SaveImage(ILogger logger, Camera camera, SKBitmap image, s if (!Directory.Exists(directory)) { - logger.LogInformation($"{camera}: Creating directory '{directory}'."); + logger.LogInformation("{camera}: Creating directory '{directory}'.", + camera, + directory); Directory.CreateDirectory(directory); } @@ -204,7 +211,9 @@ private static string SaveImage(ILogger logger, Camera camera, SKBitmap image, s } string filePath = Path.Combine(directory, fileName); - logger.LogInformation($"{camera}: Saving image to '{filePath}'."); + logger.LogInformation("{camera}: Saving image to '{filePath}'.", + camera, + filePath); using (FileStream saveStream = new FileStream(filePath, FileMode.CreateNew)) { @@ -213,11 +222,17 @@ private static string SaveImage(ILogger logger, Camera camera, SKBitmap image, s if (saved) { - logger.LogInformation($"{camera}: Image saved to '{filePath}' ({stopwatch.ElapsedMilliseconds}ms)."); + logger.LogInformation("{camera}: Image saved to '{filePath}' ({stopwatchElapsedMilliseconds}ms).", + camera, + filePath, + stopwatch.ElapsedMilliseconds); } else { - logger.LogInformation($"{camera}: Failed to save image to '{filePath}' ({stopwatch.ElapsedMilliseconds}ms)."); + logger.LogInformation("{camera}: Failed to save image to '{filePath}' ({stopwatchElapsedMilliseconds}ms).", + camera, + filePath, + stopwatch.ElapsedMilliseconds); } } return filePath; diff --git a/SynoAI/Services/SynologyService.cs b/SynoAI/Services/SynologyService.cs index 15b7d4c..cd1ab38 100644 --- a/SynoAI/Services/SynologyService.cs +++ b/SynoAI/Services/SynologyService.cs @@ -61,11 +61,16 @@ public async Task GetEndPointsAsync() // Find the Authentication entry point if (response.Data.TryGetValue(API_LOGIN, out SynologyApiInfo loginInfo)) { - _logger.LogDebug($"API: Found path '{loginInfo.Path}' for {API_LOGIN}"); + _logger.LogDebug("API: Found path '{loginInfoPath}' for {API_LOGIN}", + loginInfo.Path, + API_LOGIN); if (loginInfo.MaxVersion < Config.ApiVersionAuth) { - _logger.LogError($"API: {API_CAMERA} only supports a max version of {loginInfo.MaxVersion}, but the system is set to use version {Config.ApiVersionAuth}."); + _logger.LogError("API: {API_CAMERA} only supports a max version of {loginInfoMaxVersion}, but the system is set to use version {ConfigApiVersionAuth}.", + API_CAMERA, + loginInfo.MaxVersion, + Config.ApiVersionAuth); } } else @@ -76,11 +81,16 @@ public async Task GetEndPointsAsync() // Find the Camera entry point if (response.Data.TryGetValue(API_CAMERA, out SynologyApiInfo cameraInfo)) { - _logger.LogDebug($"API: Found path '{cameraInfo.Path}' for {API_CAMERA}"); + _logger.LogDebug("API: Found path '{cameraInf.Path}' for {API_CAMERA}", + cameraInfo.Path, + API_CAMERA); if (cameraInfo.MaxVersion < Config.ApiVersionCamera) { - _logger.LogError($"API: {API_CAMERA} only supports a max version of {cameraInfo.MaxVersion}, but the system is set to use version {Config.ApiVersionCamera}."); + _logger.LogError("API: {API_CAMERA} only supports a max version of {cameraInfoMaxVersion}, but the system is set to use version {ConfigApiVersionCamera}.", + API_CAMERA, + cameraInfo.MaxVersion, + Config.ApiVersionCamera); } } else @@ -96,12 +106,14 @@ public async Task GetEndPointsAsync() } else { - _logger.LogError($"API: Failed due to error code '{response.Error.Code}'"); + _logger.LogError("API: Failed due to error code '{responseErrorCode}'", + response.Error.Code); } } else { - _logger.LogError($"API: Failed due to HTTP status code '{result.StatusCode}'"); + _logger.LogError("API: Failed due to HTTP status code '{resultStatusCode}'", + result.StatusCode); } } return false; @@ -116,7 +128,8 @@ public async Task LoginAsync() _logger.LogInformation("Login: Authenticating"); string loginUri = string.Format(URI_LOGIN, _loginPath, Config.ApiVersionAuth, Config.Username, SanitisePassword(Config.Password)); - _logger.LogDebug($"Login: Logging in ({loginUri})"); + _logger.LogDebug("Login: Logging in ({loginUri})", + loginUri); CookieContainer cookieContainer = new CookieContainer(); using (HttpClient httpClient = GetHttpClient(Config.Url, cookieContainer)) @@ -140,12 +153,14 @@ public async Task LoginAsync() } else { - _logger.LogError($"Login: Failed due to Synology API error code '{response.Error.Code}'"); + _logger.LogError("Login: Failed due to Synology API error code '{responseErrorCode}'", + response.Error.Code); } } else { - _logger.LogError($"Login: Failed due to HTTP status code '{result.StatusCode}'"); + _logger.LogError("Login: Failed due to HTTP status code '{resultStatusCode}'", + result.StatusCode); } } return null; @@ -179,12 +194,14 @@ public async Task> GetCamerasAsync() SynologyResponse response = await GetResponse(result); if (response.Success) { - _logger.LogInformation($"GetCameras: Successful. Found {response.Data.Cameras.Count()} cameras."); + _logger.LogInformation("GetCameras: Successful. Found {responseDataCamerasCount} cameras.", + response.Data.Cameras.Count()); return response.Data.Cameras; } else { - _logger.LogError($"GetCameras: Failed due to error code '{response.Error.Code}'"); + _logger.LogError("GetCameras: Failed due to error code '{responseErrorCode}'", + response.Error.Code); } } @@ -204,12 +221,17 @@ public async Task TakeSnapshotAsync(string cameraName) if (Cameras.TryGetValue(cameraName, out int id)) { - _logger.LogDebug($"{cameraName}: Found with Synology ID '{id}'."); + _logger.LogDebug("{cameraName}: Found with Synology ID '{id}'.", + cameraName + ,id); string resource = string.Format(URI_CAMERA_SNAPSHOT + $"&profileType={(int)Config.Quality}", _cameraPath, Config.ApiVersionCamera, id); - _logger.LogDebug($"{cameraName}: Taking snapshot from '{resource}'."); + _logger.LogDebug("{cameraName}: Taking snapshot from '{resource}'.", + cameraName, + resource); - _logger.LogInformation($"{cameraName}: Taking snapshot"); + _logger.LogInformation("{cameraName}: Taking snapshot", + cameraName); using (HttpResponseMessage response = await client.GetAsync(resource, HttpCompletionOption.ResponseHeadersRead)) { response.EnsureSuccessStatusCode(); @@ -217,7 +239,8 @@ public async Task TakeSnapshotAsync(string cameraName) if (response.Content.Headers.ContentType.MediaType == "image/jpeg") { // Only return the bytes when we have a valid image back - _logger.LogDebug($"{cameraName}: Reading snapshot"); + _logger.LogDebug("{cameraName}: Reading snapshot", + cameraName); return await response.Content.ReadAsByteArrayAsync(); } else @@ -227,18 +250,22 @@ public async Task TakeSnapshotAsync(string cameraName) if (errorResponse.Success) { // This should never happen, but let's add logging just in case - _logger.LogError($"{cameraName}: Failed to get snapshot, but the API reported success."); + _logger.LogError("{cameraName}: Failed to get snapshot, but the API reported success.", + cameraName); } else { - _logger.LogError($"{cameraName}: Failed to get snapshot with error code '{errorResponse.Error.Code}'"); + _logger.LogError("{cameraName}: Failed to get snapshot with error code '{errorResponseErrorCode}'", + cameraName, + errorResponse.Error.Code); } } } } else { - _logger.LogError($"The camera with the name '{cameraName}' was not found in the Synology camera list."); + _logger.LogError("The camera with the name '{cameraName}' was not found in the Synology camera list.", + cameraName); } return null; @@ -322,7 +349,8 @@ public async Task InitialiseAsync() SynologyCamera match = synologyCameras.FirstOrDefault(x => x.GetName().Equals(camera.Name, StringComparison.OrdinalIgnoreCase)); if (match == null) { - _logger.LogWarning($"Initialise: The camera with the name '{camera.Name}' was not found in the Surveillance Station camera list."); + _logger.LogWarning("Initialise: The camera with the name '{cameraName}' was not found in the Surveillance Station camera list.", + camera.Name); } else { diff --git a/SynoAI/SynoAI.csproj b/SynoAI/SynoAI.csproj index 57bdfb2..9d34885 100644 --- a/SynoAI/SynoAI.csproj +++ b/SynoAI/SynoAI.csproj @@ -30,14 +30,14 @@ - - - - - - - - + + + + + + + + diff --git a/SynoAI/packages.lock.json b/SynoAI/packages.lock.json index dfe79fb..fa1428e 100644 --- a/SynoAI/packages.lock.json +++ b/SynoAI/packages.lock.json @@ -4,60 +4,60 @@ "net7.0": { "MailKit": { "type": "Direct", - "requested": "[3.4.3, )", - "resolved": "3.4.3", - "contentHash": "Iewef8mcE1B1LrVudxQjQ0LcriPPeTbxmWMoHQzFS+P6TpEY2eVDbdKdB0Qnbmqr/5w7WfK2mNWuoSX9pI470g==", + "requested": "[4.2.0, )", + "resolved": "4.2.0", + "contentHash": "NXm66YkEHyLXSyH1Ga/dUS8SB0vYTlGESUluLULa7pG0/eK8c/R9JzMyH0KbKQsgpLGwbji9quAlrcUOL0OjPA==", "dependencies": { - "MimeKit": "3.4.3" + "MimeKit": "4.2.0" } }, "Microsoft.VisualStudio.Azure.Containers.Tools.Targets": { "type": "Direct", - "requested": "[1.17.0, )", - "resolved": "1.17.0", - "contentHash": "gfDtAL1WhkjbRdbZlt/ZeQYCbgRpNCZCGj+yeqHObsNFRDHjq8qZJOX9AyTxJpSRYMi9SJk7JDyAbbVYRgEhAA==" + "requested": "[1.19.5, )", + "resolved": "1.19.5", + "contentHash": "Kaa1rBZdJFq5A0qgAcl6Bmk/UqLXTq9acEqxUlPEBA8oscmakLfkvuSXfG7Wa9t1/keaT85EuuDNgOo+Z9VYOQ==" }, "MQTTnet": { "type": "Direct", - "requested": "[4.1.4.563, )", - "resolved": "4.1.4.563", - "contentHash": "gO9segUcKyQJcjV7w7OOdoAIkec7cUN65vEhYutbdWcj4rbtz/oL/RDvQVVbameXc6ChkjKx7/HbO+R8ejAUZQ==" + "requested": "[4.3.1.873, )", + "resolved": "4.3.1.873", + "contentHash": "5Btmzjv9TWQewlHL6QPB3/deTxAfHf0cR1ixehH/4311oKUpiYrgt1uQZFTbyBWjR7zVKv1U3+s4o3IPm/++Ww==" }, "Newtonsoft.Json": { "type": "Direct", - "requested": "[13.0.2, )", - "resolved": "13.0.2", - "contentHash": "R2pZ3B0UjeyHShm9vG+Tu0EBb2lC8b0dFzV9gVn50ofHXh9Smjk6kTn7A/FdAsC8B5cKib1OnGYOXxRBz5XQDg==" + "requested": "[13.0.3, )", + "resolved": "13.0.3", + "contentHash": "HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==" }, "SkiaSharp": { "type": "Direct", - "requested": "[2.88.3, )", - "resolved": "2.88.3", - "contentHash": "GG8X3EdfwyBfwjl639UIiOVOKEdeoqDgYrz0P1MUCnefXt9cofN+AK8YB/v1+5cLMr03ieWCQdDmPqnFIzSxZw==", + "requested": "[2.88.5, )", + "resolved": "2.88.5", + "contentHash": "43t9YEvcZtT+tuMN4hHH8rV8h7ttk1DRv5Tptxamy+bPuE8Up51+ME1Y1TXYAQf75686tHO488a0YKAnmJaSNg==", "dependencies": { - "SkiaSharp.NativeAssets.Win32": "2.88.3", - "SkiaSharp.NativeAssets.macOS": "2.88.3" + "SkiaSharp.NativeAssets.Win32": "2.88.5", + "SkiaSharp.NativeAssets.macOS": "2.88.5" } }, "SkiaSharp.NativeAssets.Linux": { "type": "Direct", - "requested": "[2.88.3, )", - "resolved": "2.88.3", - "contentHash": "wz29evZVWRqN7WHfenFwQIgqtr8f5vHCutcl1XuhWrHTRZeaIBk7ngjhyHpjUMcQxtIEAdq34ZRvMQshsBYjqg==", + "requested": "[2.88.5, )", + "resolved": "2.88.5", + "contentHash": "VMxHc9M9ENUJA7ZZ5q5HFsYZN7DjWtlokrP0pgGqVKX/SVk858s8LiO1yoa4PGde82JJh2y8tVjFpQ6zCcSa3w==", "dependencies": { - "SkiaSharp": "2.88.3" + "SkiaSharp": "2.88.5" } }, "Swashbuckle.AspNetCore": { "type": "Direct", - "requested": "[6.4.0, )", - "resolved": "6.4.0", - "contentHash": "eUBr4TW0up6oKDA5Xwkul289uqSMgY0xGN4pnbOIBqCcN9VKGGaPvHX3vWaG/hvocfGDP+MGzMA0bBBKz2fkmQ==", + "requested": "[6.5.0, )", + "resolved": "6.5.0", + "contentHash": "FK05XokgjgwlCI6wCT+D4/abtQkL1X1/B9Oas6uIwHFmYrIO9WUD5aLC9IzMs9GnHfUXOtXZ2S43gN1mhs5+aA==", "dependencies": { "Microsoft.Extensions.ApiDescription.Server": "6.0.5", - "Swashbuckle.AspNetCore.Swagger": "6.4.0", - "Swashbuckle.AspNetCore.SwaggerGen": "6.4.0", - "Swashbuckle.AspNetCore.SwaggerUI": "6.4.0" + "Swashbuckle.AspNetCore.Swagger": "6.5.0", + "Swashbuckle.AspNetCore.SwaggerGen": "6.5.0", + "Swashbuckle.AspNetCore.SwaggerUI": "6.5.0" } }, "System.Drawing.Common": { @@ -71,13 +71,18 @@ }, "Telegram.Bot": { "type": "Direct", - "requested": "[18.0.0, )", - "resolved": "18.0.0", - "contentHash": "BD0UchUXINymCGS+1O1tv2enCRyv+VbSJQAgfnueTZs3j7K4XXyJyW0CgyJleTrqB1oq1hS1ux6gBpi3Ajp+ZQ==", + "requested": "[19.0.0, )", + "resolved": "19.0.0", + "contentHash": "Q16IOitgjGoaJOuqgKQy0FeF+hr/ncmlX2esrhCC7aiyhSX7roYEriWaGAHkQZR8QzbImjFfl4eQh2IxcnOrPg==", "dependencies": { - "Newtonsoft.Json": "12.0.2" + "Newtonsoft.Json": "13.0.1" } }, + "BouncyCastle.Cryptography": { + "type": "Transitive", + "resolved": "2.2.1", + "contentHash": "A6Zr52zVqJKt18ZBsTnX0qhG0kwIQftVAjLmszmkiR/trSp8H+xj1gUOzk7XHwaKgyREMSV1v9XaKrBUeIOdvQ==" + }, "Microsoft.Extensions.ApiDescription.Server": { "type": "Transitive", "resolved": "6.0.5", @@ -95,55 +100,50 @@ }, "MimeKit": { "type": "Transitive", - "resolved": "3.4.3", - "contentHash": "7TSAcziEwk0bGWODpFTQASghXfYNBBa5VdM8KO4s5SBp5LYgIVXcQsdLBpPQ2XhZW74wfaX8RBUMs1GTMlLJcA==", + "resolved": "4.2.0", + "contentHash": "HlfWiJ6t40r8u/rCK2p/8dm1ILiWw4XHucm2HImDYIFS3uZe7IKZyaCDafEoZR7VG7AW1JQxNPQCAxmAnJfRvA==", "dependencies": { - "Portable.BouncyCastle": "1.9.0", + "BouncyCastle.Cryptography": "2.2.1", "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Security.Cryptography.Pkcs": "6.0.0", - "System.Text.Encoding.CodePages": "6.0.0" + "System.Security.Cryptography.Pkcs": "7.0.2", + "System.Text.Encoding.CodePages": "7.0.0" } }, - "Portable.BouncyCastle": { - "type": "Transitive", - "resolved": "1.9.0", - "contentHash": "eZZBCABzVOek+id9Xy04HhmgykF0wZg9wpByzrWN7q8qEI0Qen9b7tfd7w8VA3dOeesumMG7C5ZPy0jk7PSRHw==" - }, "SkiaSharp.NativeAssets.macOS": { "type": "Transitive", - "resolved": "2.88.3", - "contentHash": "CEbWAXMGFkPV3S1snBKK7jEG3+xud/9kmSAhu0BEUKKtlMdxx+Qal0U9bntQREM9QpqP5xLWZooodi8IlV8MEg==" + "resolved": "2.88.5", + "contentHash": "snAA6ghUHBeXSOEa/lbjYMUPDKdNh4YRjU+dBoU85EMDEPlAwtkM9gESTM8FKY67ozqX8tqLzad1RRDNistGsg==" }, "SkiaSharp.NativeAssets.Win32": { "type": "Transitive", - "resolved": "2.88.3", - "contentHash": "MU4ASL8VAbTv5vSw1PoiWjjjpjtGhWtFYuJnrN4sNHFCePb2ohQij9JhSdqLLxk7RpRtWPdV93fbA53Pt+J0yw==" + "resolved": "2.88.5", + "contentHash": "U0GuMsdtdbHjVmuyTPRbb+ifaTPYBIV8WvPFx81ZHwzfL2TVe8SQjum8AMXSZDDlf90rhxOC5slzaL33uoHUhA==" }, "Swashbuckle.AspNetCore.Swagger": { "type": "Transitive", - "resolved": "6.4.0", - "contentHash": "nl4SBgGM+cmthUcpwO/w1lUjevdDHAqRvfUoe4Xp/Uvuzt9mzGUwyFCqa3ODBAcZYBiFoKvrYwz0rabslJvSmQ==", + "resolved": "6.5.0", + "contentHash": "XWmCmqyFmoItXKFsQSwQbEAsjDKcxlNf1l+/Ki42hcb6LjKL8m5Db69OTvz5vLonMSRntYO1XLqz0OP+n3vKnA==", "dependencies": { "Microsoft.OpenApi": "1.2.3" } }, "Swashbuckle.AspNetCore.SwaggerGen": { "type": "Transitive", - "resolved": "6.4.0", - "contentHash": "lXhcUBVqKrPFAQF7e/ZeDfb5PMgE8n5t6L5B6/BQSpiwxgHzmBcx8Msu42zLYFTvR5PIqE9Q9lZvSQAcwCxJjw==", + "resolved": "6.5.0", + "contentHash": "Y/qW8Qdg9OEs7V013tt+94OdPxbRdbhcEbw4NiwGvf4YBcfhL/y7qp/Mjv/cENsQ2L3NqJ2AOu94weBy/h4KvA==", "dependencies": { - "Swashbuckle.AspNetCore.Swagger": "6.4.0" + "Swashbuckle.AspNetCore.Swagger": "6.5.0" } }, "Swashbuckle.AspNetCore.SwaggerUI": { "type": "Transitive", - "resolved": "6.4.0", - "contentHash": "1Hh3atb3pi8c+v7n4/3N80Jj8RvLOXgWxzix6w3OZhB7zBGRwsy7FWr4e3hwgPweSBpwfElqj4V4nkjYabH9nQ==" + "resolved": "6.5.0", + "contentHash": "OvbvxX+wL8skxTBttcBsVxdh73Fag4xwqEU2edh4JMn7Ws/xJHnY/JB1e9RoCb6XpDxUF3hD9A0Z1lEUx40Pfw==" }, "System.Formats.Asn1": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "T6fD00dQ3NTbPDy31m4eQUwKW84s03z0N2C8HpOklyeaDgaJPa/TexP4/SkORMSOwc7WhKifnA6Ya33AkzmafA==" + "resolved": "7.0.0", + "contentHash": "+nfpV0afLmvJW8+pLlHxRjz3oZJw4fkyU9MMEaMhCsHi/SN9bGF9q79ROubDiwTiCHezmK0uCWkPP7tGFP/4yg==" }, "System.Runtime.CompilerServices.Unsafe": { "type": "Transitive", @@ -152,19 +152,16 @@ }, "System.Security.Cryptography.Pkcs": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "elM3x+xSRhzQysiqo85SbidJJ2YbZlnvmh+53TuSZHsD7dNuuEWser+9EFtY+rYupBwkq2avc6ZCO3/6qACgmg==", + "resolved": "7.0.2", + "contentHash": "xhFNJOcQSWhpiVGLLBQYoxAltQSQVycMkwaX1z7I7oEdT9Wr0HzSM1yeAbfoHaERIYd5s6EpLSOLs2qMchSKlA==", "dependencies": { - "System.Formats.Asn1": "6.0.0" + "System.Formats.Asn1": "7.0.0" } }, "System.Text.Encoding.CodePages": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "ZFCILZuOvtKPauZ/j/swhvw68ZRi9ATCfvGbk1QfydmcXBkIWecWKn/250UH7rahZ5OoDBaiAudJtPvLwzw85A==", - "dependencies": { - "System.Runtime.CompilerServices.Unsafe": "6.0.0" - } + "resolved": "7.0.0", + "contentHash": "LSyCblMpvOe0N3E+8e0skHcrIhgV2huaNcjUUEa8hRtgEAm36aGkRoC8Jxlb6Ra6GSfF29ftduPNywin8XolzQ==" } } } From 213a4a731d03709a24209bf0f68d0cc7a374ff0f Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Sun, 10 Sep 2023 16:12:20 +0200 Subject: [PATCH 02/13] Added the insecure URL from djdd87 changes. --- SynoAI/Notifiers/Webhook/Webhook.cs | 81 ++++++++++++++++------ SynoAI/Notifiers/Webhook/WebhookFactory.cs | 6 +- 2 files changed, 63 insertions(+), 24 deletions(-) diff --git a/SynoAI/Notifiers/Webhook/Webhook.cs b/SynoAI/Notifiers/Webhook/Webhook.cs index 79e3fbc..67e86fa 100644 --- a/SynoAI/Notifiers/Webhook/Webhook.cs +++ b/SynoAI/Notifiers/Webhook/Webhook.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json; using SynoAI.Models; using System.Net.Http.Headers; +using System.Net; using System.Text; namespace SynoAI.Notifiers.Webhook @@ -46,6 +47,11 @@ internal class Webhook : NotifierBase /// public bool SendImage { get; set; } + /// + /// Allow insecure URL Access to the API. + /// + public bool AllowInsecureUrl { get; set; } + /// /// Sends a notification to the Webhook. /// @@ -56,7 +62,8 @@ public override async Task SendAsync(Camera camera, Notification notification, I { logger.LogInformation("{cameraName}: Webhook: Processing", camera.Name); - using (HttpClient client = new()) + using (HttpClient client = GetHttpClient()) + { FileStream fileStream = null; client.DefaultRequestHeaders.Authorization = GetAuthenticationHeader(); @@ -109,28 +116,39 @@ public override async Task SendAsync(Camera camera, Notification notification, I Method); HttpResponseMessage response; - switch (Method) + try { - case "DELETE": - response = await client.DeleteAsync(Url); - break; - case "GET": - response = await client.GetAsync(Url); - break; - case "PATCH": - response = await client.PatchAsync(Url, content); - break; - case "POST": - response = await client.PostAsync(Url, content); - break; - case "PUT": - response = await client.PutAsync(Url, content); - break; - default: - logger.LogError("{cameraName}: Webhook: The method type '{Method}' is not supported.", - camera.Name, - Method); - return; + switch (Method) + { + case "DELETE": + response = await client.DeleteAsync(Url); + break; + case "GET": + response = await client.GetAsync(Url); + break; + case "PATCH": + response = await client.PatchAsync(Url, content); + break; + case "POST": + response = await client.PostAsync(Url, content); + break; + case "PUT": + response = await client.PutAsync(Url, content); + break; + default: + logger.LogError("{camera.Name}: Webhook: The method type '{Method}' is not supported.", + camera.Name, + Method); + return; + } + } + catch (Exception ex) + { + logger.LogError("{cameraName}: Webhook: Unhandled Exception occurred '{exMessage}'.", + camera.Name, + ex.Message); + return; + } if (response.IsSuccessStatusCode) @@ -155,6 +173,25 @@ public override async Task SendAsync(Camera camera, Notification notification, I } } + /// + /// Gets a object for the Webhook request. + /// + /// A . + private static HttpClient GetHttpClient() + { + if (!Config.AllowInsecureUrl) + { + return new(); + } + + HttpClientHandler httpClientHandler = new() + { + ServerCertificateCustomValidationCallback = (message, cert, chain, sslPolicyErrors) => true + }; + return new(httpClientHandler); + } + + /// /// Generates an authentication header for the client. /// diff --git a/SynoAI/Notifiers/Webhook/WebhookFactory.cs b/SynoAI/Notifiers/Webhook/WebhookFactory.cs index 3ae5779..34629c0 100644 --- a/SynoAI/Notifiers/Webhook/WebhookFactory.cs +++ b/SynoAI/Notifiers/Webhook/WebhookFactory.cs @@ -17,15 +17,17 @@ public override INotifier Create(ILogger logger, IConfigurationSection section) string method = section.GetValue("Method", "POST"); bool sendImage = section.GetValue("SendImage", true); bool sendTypes = section.GetValue("SendTypes", false); + bool allowInsecureUrl = section.GetValue("AllowInsecureUrl", false); - Webhook webhook = new Webhook() + Webhook webhook = new() { Url = url, Authentication = authentication, Username = username, Password = password, Token = token, - SendImage = sendImage + SendImage = sendImage, + AllowInsecureUrl = allowInsecureUrl }; if (!string.IsNullOrWhiteSpace(imageField)) From 8450a8ddb46993f4197c7cc29fe7894fe17d09b4 Mon Sep 17 00:00:00 2001 From: sanderdewit Date: Sun, 10 Sep 2023 16:24:03 +0200 Subject: [PATCH 03/13] Update docker-image.yml --- .github/workflows/docker-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index 7bb5d62..12c8a8a 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -16,8 +16,8 @@ jobs: steps: - uses: actions/checkout@v2 - name: Build the Docker image - run: docker build -t djdd87/synoai:latest SynoAI + run: docker build -t dewitauto/synoai-fork:latest SynoAI - name: Login to Docker Hub run: docker login -u="${{ secrets.DOCKERHUB_USERNAME }}" -p="${{ secrets.DOCKERHUB_PASSWORD }}" - name: Publish the Docker image - run: docker push djdd87/synoai:latest + run: docker push dewitauto/synoai-fork:latest From 83f52d325456a70df695af12e9b5fe566d8cf11b Mon Sep 17 00:00:00 2001 From: sanderdewit Date: Sun, 10 Sep 2023 16:30:49 +0200 Subject: [PATCH 04/13] Create docker-imagetest.yml --- .github/workflows/docker-imagetest.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/docker-imagetest.yml diff --git a/.github/workflows/docker-imagetest.yml b/.github/workflows/docker-imagetest.yml new file mode 100644 index 0000000..4df106a --- /dev/null +++ b/.github/workflows/docker-imagetest.yml @@ -0,0 +1,18 @@ +name: Docker Image CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - name: Build the Docker image + run: docker build . --file Dockerfile --tag dewitauto:test From b10e1eced87816f0a4fa14d44ba18f7e5f86a126 Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Sun, 10 Sep 2023 16:58:19 +0200 Subject: [PATCH 05/13] added timestamp to logging, fixed notification to include confidence. --- SynoAI/Notifiers/NotifierBase.cs | 45 +++++++++----------------- SynoAI/Program.cs | 17 +++++++++- SynoAI/TimestampLoggerProvider.cs | 52 +++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 31 deletions(-) create mode 100644 SynoAI/TimestampLoggerProvider.cs diff --git a/SynoAI/Notifiers/NotifierBase.cs b/SynoAI/Notifiers/NotifierBase.cs index 2b84448..468b973 100644 --- a/SynoAI/Notifiers/NotifierBase.cs +++ b/SynoAI/Notifiers/NotifierBase.cs @@ -17,55 +17,40 @@ internal abstract class NotifierBase : INotifier public virtual Task CleanupAsync(ILogger logger) { return Task.CompletedTask; } protected static string GetMessage(Camera camera, IEnumerable foundTypes, List predictions, string errorMessage = null) - { - string result ; + string result; + if (Config.AlternativeLabelling && Config.DrawMode == DrawMode.Matches) { - // Defaulting into generic label type - String typeLabel = "object"; - - if (camera.Types.Count() == 1) { - // Only one object type configured: use it instead of generic "object" label - typeLabel = camera.Types.First(); - } + // Defaulting into a generic label type + string typeLabel = foundTypes.Count() == 1 ? foundTypes.First() : "objects"; if (foundTypes.Count() > 1) { // Several objects detected - result = $"{camera.Name}: {foundTypes.Count()} {typeLabel}s\n{String.Join("\n", foundTypes.Select(x => x).ToArray())}"; - } - else - { - // Just one object detected - result = $"{camera.Name}: {foundTypes.First()}"; + result = $"{camera.Name}: {foundTypes.Count()} {typeLabel}s\n{string.Join("\n", foundTypes)}"; } - // Include prediction confidence - foreach (var prediction in predictions) + else { - result += $"\n{prediction.Label}: {prediction.Confidence}%"; + // Just one object detected + result = $"{camera.Name}: {foundTypes.First()}"; } - } - else + else { - // Standard (old) labelling - result = $"Motion detected on {camera.Name}\n\nDetected {foundTypes.Count()} objects:\n{String.Join("\n", foundTypes.Select(x => x).ToArray())}"; + // Standard (old) labeling + result = $"Motion detected on {camera.Name}\n\nDetected {foundTypes.Count()} objects:\n{string.Join("\n", foundTypes)}"; } - // Include prediction confidence + + // Include prediction confidence for each detected object foreach (var prediction in predictions) { result += $"\n{prediction.Label}: {prediction.Confidence}%"; } - - if (!string.IsNullOrWhiteSpace(errorMessage)){ - result += $"\nAn error occurred during the creation of the notification: {errorMessage}"; - } - if (predictions != null && predictions.Any()) + if (!string.IsNullOrWhiteSpace(errorMessage)) { - result += "\nPredictions:\n"; - result += string.Join("\n", predictions.Select(prediction => $"{prediction.Label} ({prediction.Confidence}%)")); + result += $"\nAn error occurred during the creation of the notification: {errorMessage}"; } return result; diff --git a/SynoAI/Program.cs b/SynoAI/Program.cs index deda1fd..20abe3f 100644 --- a/SynoAI/Program.cs +++ b/SynoAI/Program.cs @@ -1,3 +1,5 @@ +using Microsoft.Extensions.Logging; + namespace SynoAI { /// @@ -11,7 +13,20 @@ public class Program /// The command-line arguments. public static void Main(string[] args) { - CreateHostBuilder(args).Build().Run(); + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddProvider(new TimestampLoggerProvider()); + }); + + CreateHostBuilder(args) + .ConfigureLogging((context, logging) => + { + logging.ClearProviders(); // Remove any default log providers + logging.AddConsole(); // Add the console logger + }) + .Build() + .Run(); + } /// /// Main method to create and configure the host for the SynoAI application. diff --git a/SynoAI/TimestampLoggerProvider.cs b/SynoAI/TimestampLoggerProvider.cs new file mode 100644 index 0000000..c5dcb72 --- /dev/null +++ b/SynoAI/TimestampLoggerProvider.cs @@ -0,0 +1,52 @@ +namespace SynoAI +{ + using Microsoft.Extensions.Logging; + using System; + + public class TimestampLoggerProvider : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) + { + return new TimestampLogger(); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Dispose managed resources here + } + + // Dispose unmanaged resources here + + // Call base.Dispose() if this class has a base class that implements IDisposable + } + + } + + public class TimestampLogger : ILogger + { + public IDisposable BeginScope(TState state) => null; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) + { + // Get the current timestamp + var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); + + // Include the timestamp in the log message + var logMessage = $"{timestamp} [{logLevel}] {formatter(state, exception)}"; + + // Write the log message to the console or any other log destination + Console.WriteLine(logMessage); + } + } + +} From a257e13eef130b7621f09fe16ed40576669b457f Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Sun, 10 Sep 2023 17:08:42 +0200 Subject: [PATCH 06/13] fixing timestamps? --- SynoAI/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/SynoAI/Program.cs b/SynoAI/Program.cs index 20abe3f..437c4e4 100644 --- a/SynoAI/Program.cs +++ b/SynoAI/Program.cs @@ -22,7 +22,6 @@ public static void Main(string[] args) .ConfigureLogging((context, logging) => { logging.ClearProviders(); // Remove any default log providers - logging.AddConsole(); // Add the console logger }) .Build() .Run(); From 8a98462ee42126765aa446c1ce57ede52ef14c55 Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Sun, 10 Sep 2023 17:15:11 +0200 Subject: [PATCH 07/13] fixing default logger to console --- SynoAI/TimestampLoggerProvider.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/SynoAI/TimestampLoggerProvider.cs b/SynoAI/TimestampLoggerProvider.cs index c5dcb72..6d1249b 100644 --- a/SynoAI/TimestampLoggerProvider.cs +++ b/SynoAI/TimestampLoggerProvider.cs @@ -32,6 +32,16 @@ protected virtual void Dispose(bool disposing) public class TimestampLogger : ILogger { + private readonly ILogger _consoleLogger; // Add a console logger + + public TimestampLogger() + { + _consoleLogger = LoggerFactory.Create(builder => + { + builder.AddConsole(); + }).CreateLogger("Console"); + } + public IDisposable BeginScope(TState state) => null; public bool IsEnabled(LogLevel logLevel) => true; @@ -44,9 +54,8 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except // Include the timestamp in the log message var logMessage = $"{timestamp} [{logLevel}] {formatter(state, exception)}"; - // Write the log message to the console or any other log destination - Console.WriteLine(logMessage); + // Write the log message to the console + _consoleLogger.Log(logLevel, eventId, state, exception, (_, _) => logMessage); } } - } From 53488b787f50bd8d8518e1aef391fbbc91e3837c Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Sun, 10 Sep 2023 17:45:28 +0200 Subject: [PATCH 08/13] fixed timestamp logging --- SynoAI/Notifiers/Pushover/Pushover.cs | 4 ++-- SynoAI/Program.cs | 1 + SynoAI/Properties/launchSettings.json | 28 ++++++++++++++++++--------- 3 files changed, 22 insertions(+), 11 deletions(-) diff --git a/SynoAI/Notifiers/Pushover/Pushover.cs b/SynoAI/Notifiers/Pushover/Pushover.cs index df8b766..c2622ae 100644 --- a/SynoAI/Notifiers/Pushover/Pushover.cs +++ b/SynoAI/Notifiers/Pushover/Pushover.cs @@ -55,8 +55,8 @@ public override async Task SendAsync(Camera camera, Notification notification, I logger.LogInformation("{cameraName}: Pushover: Building message", camera.Name); - //string message = GetMessage(camera, notification.FoundTypes); - string message = GetMessage(camera, notification.FoundTypes, new List()); + //string message = GetMessage(camera, notification.FoundTypes, new List()); + string message = GetMessage(camera, notification.FoundTypes, notification.ValidPredictions.ToList()); string device = Devices == null || !Devices.Any() ? String.Empty : string.Join(',', Devices); string title = $"{camera.Name}: Movement Detected"; diff --git a/SynoAI/Program.cs b/SynoAI/Program.cs index 437c4e4..20abe3f 100644 --- a/SynoAI/Program.cs +++ b/SynoAI/Program.cs @@ -22,6 +22,7 @@ public static void Main(string[] args) .ConfigureLogging((context, logging) => { logging.ClearProviders(); // Remove any default log providers + logging.AddConsole(); // Add the console logger }) .Build() .Run(); diff --git a/SynoAI/Properties/launchSettings.json b/SynoAI/Properties/launchSettings.json index 3d4e1b2..798eaaf 100644 --- a/SynoAI/Properties/launchSettings.json +++ b/SynoAI/Properties/launchSettings.json @@ -1,13 +1,4 @@ { - "$schema": "http://json.schemastore.org/launchsettings.json", - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:50639", - "sslPort": 44365 - } - }, "profiles": { "IIS Express": { "commandName": "IISExpress", @@ -33,6 +24,25 @@ "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", "publishAllPorts": true, "useSSL": true + }, + "WSL": { + "commandName": "WSL2", + "launchBrowser": true, + "launchUrl": "https://localhost:5001/swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASPNETCORE_URLS": "https://localhost:5001;http://localhost:5000" + }, + "distributionName": "" + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:50639", + "sslPort": 44365 } } } \ No newline at end of file From fa0cb9d61972c87bfd0c7a994e69bfcddb01bd13 Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Sun, 10 Sep 2023 18:03:08 +0200 Subject: [PATCH 09/13] timestamp another try --- SynoAI/TimestampLoggerProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SynoAI/TimestampLoggerProvider.cs b/SynoAI/TimestampLoggerProvider.cs index 6d1249b..bc78dfa 100644 --- a/SynoAI/TimestampLoggerProvider.cs +++ b/SynoAI/TimestampLoggerProvider.cs @@ -55,7 +55,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except var logMessage = $"{timestamp} [{logLevel}] {formatter(state, exception)}"; // Write the log message to the console - _consoleLogger.Log(logLevel, eventId, state, exception, (_, _) => logMessage); + Console.WriteLine(logMessage); } } } From c139dbe4aa1f605253835be793ccf6e109c38d33 Mon Sep 17 00:00:00 2001 From: sanderdewit Date: Sun, 10 Sep 2023 18:05:16 +0200 Subject: [PATCH 10/13] Delete .github/workflows/docker-imagetest.yml --- .github/workflows/docker-imagetest.yml | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 .github/workflows/docker-imagetest.yml diff --git a/.github/workflows/docker-imagetest.yml b/.github/workflows/docker-imagetest.yml deleted file mode 100644 index 4df106a..0000000 --- a/.github/workflows/docker-imagetest.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Docker Image CI - -on: - push: - branches: [ "main" ] - pull_request: - branches: [ "main" ] - -jobs: - - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - name: Build the Docker image - run: docker build . --file Dockerfile --tag dewitauto:test From 142befafd488abf628c6ec83a68391e56001a424 Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Sun, 10 Sep 2023 18:41:47 +0200 Subject: [PATCH 11/13] layer 8 issue with timestamp, all resolved :-) --- SynoAI/Program.cs | 20 ++-------- SynoAI/TimestampLoggerProvider.cs | 61 ------------------------------- 2 files changed, 3 insertions(+), 78 deletions(-) delete mode 100644 SynoAI/TimestampLoggerProvider.cs diff --git a/SynoAI/Program.cs b/SynoAI/Program.cs index 20abe3f..c516bf3 100644 --- a/SynoAI/Program.cs +++ b/SynoAI/Program.cs @@ -1,5 +1,3 @@ -using Microsoft.Extensions.Logging; - namespace SynoAI { /// @@ -13,26 +11,14 @@ public class Program /// The command-line arguments. public static void Main(string[] args) { - var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddProvider(new TimestampLoggerProvider()); - }); - - CreateHostBuilder(args) - .ConfigureLogging((context, logging) => - { - logging.ClearProviders(); // Remove any default log providers - logging.AddConsole(); // Add the console logger - }) - .Build() - .Run(); - + CreateHostBuilder(args).Build().Run(); } /// /// Main method to create and configure the host for the SynoAI application. /// /// Command-line arguments. /// An that configures the host for the application. + public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder => @@ -40,4 +26,4 @@ public static IHostBuilder CreateHostBuilder(string[] args) => webBuilder.UseStartup(); }); } -} +} \ No newline at end of file diff --git a/SynoAI/TimestampLoggerProvider.cs b/SynoAI/TimestampLoggerProvider.cs deleted file mode 100644 index bc78dfa..0000000 --- a/SynoAI/TimestampLoggerProvider.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace SynoAI -{ - using Microsoft.Extensions.Logging; - using System; - - public class TimestampLoggerProvider : ILoggerProvider - { - public ILogger CreateLogger(string categoryName) - { - return new TimestampLogger(); - } - - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - // Dispose managed resources here - } - - // Dispose unmanaged resources here - - // Call base.Dispose() if this class has a base class that implements IDisposable - } - - } - - public class TimestampLogger : ILogger - { - private readonly ILogger _consoleLogger; // Add a console logger - - public TimestampLogger() - { - _consoleLogger = LoggerFactory.Create(builder => - { - builder.AddConsole(); - }).CreateLogger("Console"); - } - - public IDisposable BeginScope(TState state) => null; - - public bool IsEnabled(LogLevel logLevel) => true; - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - // Get the current timestamp - var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"); - - // Include the timestamp in the log message - var logMessage = $"{timestamp} [{logLevel}] {formatter(state, exception)}"; - - // Write the log message to the console - Console.WriteLine(logMessage); - } - } -} From 6ddd94f1a899ce9c50c845f2cee33fc6bffcebc8 Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Thu, 14 Sep 2023 16:31:20 +0200 Subject: [PATCH 12/13] Modified message to not have a duplication of objects. instead of person and person: percentage, just person: percentage --- SynoAI/Notifiers/NotifierBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SynoAI/Notifiers/NotifierBase.cs b/SynoAI/Notifiers/NotifierBase.cs index 468b973..1e2503b 100644 --- a/SynoAI/Notifiers/NotifierBase.cs +++ b/SynoAI/Notifiers/NotifierBase.cs @@ -39,7 +39,7 @@ protected static string GetMessage(Camera camera, IEnumerable foundTypes else { // Standard (old) labeling - result = $"Motion detected on {camera.Name}\n\nDetected {foundTypes.Count()} objects:\n{string.Join("\n", foundTypes)}"; + result = $"Motion detected on {camera.Name}\n\nDetected {foundTypes.Count()} objects:\n"; } // Include prediction confidence for each detected object From d1ff36ba2ba69f359a704a9ae59e7452ca3f5f60 Mon Sep 17 00:00:00 2001 From: Sander de Wit Date: Thu, 14 Sep 2023 17:04:21 +0200 Subject: [PATCH 13/13] Remove warnings except the bitmap Windows processing --- SynoAI/Controllers/CameraController.cs | 11 +++-- SynoAI/Controllers/HomeController.cs | 8 ++-- SynoAI/Controllers/ImageController.cs | 3 ++ SynoAI/Hubs/SynoAIHub.cs | 3 ++ SynoAI/Models/AIPrediction.cs | 29 ++++++++++- SynoAI/Models/DTOs/CameraOptionsDto.cs | 6 +++ SynoAI/Models/DTOs/UpdateDto.cs | 20 ++++++-- SynoAI/Models/Graph.cs | 19 ++++---- SynoAI/Models/OverlapMode.cs | 3 ++ SynoAI/Models/Point.cs | 9 ++++ SynoAI/Models/SynologyApiInfo.cs | 15 ++++++ SynoAI/Models/SynologyCamera.cs | 16 ++++++- SynoAI/Models/Zone.cs | 12 +++++ SynoAI/Notifiers/NotifierBase.cs | 6 +-- SynoAI/Notifiers/NotifierFactory.cs | 41 +++++----------- .../Pushbullet/PushbulletUploadRequest.cs | 9 ++++ .../PushbulletUploadRequestResponse.cs | 15 ++++++ SynoAI/Notifiers/Pushover/Pushover.cs | 48 +++++++++---------- SynoAI/Services/IAIService.cs | 10 ++++ SynoAI/Services/ISynologyService.cs | 15 ++++++ SynoAI/Startup.cs | 21 +++++--- SynoAI/Views/Home/Hour.cshtml | 6 +-- SynoAI/Views/Home/Index.cshtml | 6 +-- 23 files changed, 239 insertions(+), 92 deletions(-) diff --git a/SynoAI/Controllers/CameraController.cs b/SynoAI/Controllers/CameraController.cs index 9637fe1..34a0a30 100644 --- a/SynoAI/Controllers/CameraController.cs +++ b/SynoAI/Controllers/CameraController.cs @@ -81,6 +81,7 @@ public async void Get(string id) return; } + // Ensure the camera isn't under a delay lock (_delayedCameraChecks) { @@ -114,6 +115,7 @@ public async void Get(string id) } try + { // Kick off the autocleanup CleanupOldImages(); @@ -288,6 +290,7 @@ public async void Get(string id) id, overallStopwatch.ElapsedMilliseconds); } + else { // We didn't get any predictions whatsoever from the AI @@ -329,13 +332,15 @@ public async void Get(string id) } } } - + /// + /// POSTs camera options with the specified ID. + /// + /// The ID of the camera. + /// The camera options to be posted. [HttpPost] [Route("{id}")] - public void Post(string id, [FromBody]CameraOptionsDto options) - { if (options.HasChanged(x=> x.Enabled)) { diff --git a/SynoAI/Controllers/HomeController.cs b/SynoAI/Controllers/HomeController.cs index 9b1f539..228bdf5 100644 --- a/SynoAI/Controllers/HomeController.cs +++ b/SynoAI/Controllers/HomeController.cs @@ -276,13 +276,13 @@ private static GraphData AddGraphPoint(GraphData data, DateTime date, int object data.GraphPoints.Add( new GraphPoint() { Date = date, Objects = objects, Predictions = predictions }); //Adjust Max value for Y axis - if ( objects > data.yMax) + if ( objects > data.YMax) { - data.yMax = objects ; + data.YMax = objects ; } - else if ( predictions > data.yMax) + else if ( predictions > data.YMax) { - data.yMax = predictions; + data.YMax = predictions; } return data; } diff --git a/SynoAI/Controllers/ImageController.cs b/SynoAI/Controllers/ImageController.cs index e07d531..73d254a 100644 --- a/SynoAI/Controllers/ImageController.cs +++ b/SynoAI/Controllers/ImageController.cs @@ -2,6 +2,9 @@ namespace SynoAI.Controllers { + /// + /// Image Controller to get the specific file + /// public class ImageController : Controller { /// diff --git a/SynoAI/Hubs/SynoAIHub.cs b/SynoAI/Hubs/SynoAIHub.cs index 7458865..e703093 100644 --- a/SynoAI/Hubs/SynoAIHub.cs +++ b/SynoAI/Hubs/SynoAIHub.cs @@ -2,6 +2,9 @@ namespace SynoAI.Hubs { + /// + /// SynoAI Hub Class + /// public class SynoAIHub : Hub { // euquiq: This class just needs to be present for SignalR diff --git a/SynoAI/Models/AIPrediction.cs b/SynoAI/Models/AIPrediction.cs index 714fc64..96a068a 100644 --- a/SynoAI/Models/AIPrediction.cs +++ b/SynoAI/Models/AIPrediction.cs @@ -1,14 +1,37 @@ namespace SynoAI.Models { + /// +/// Represents an AI prediction. +/// public class AIPrediction { + /// + /// Gets or sets the label of the prediction. + /// public string Label { get; set; } + /// + /// Gets or sets the confidence of the prediction. + /// public decimal Confidence { get; set; } + /// + /// Gets or sets the minimum X size of the prediction. + /// public int MinX { get; set; } + /// + /// Gets or sets the minimum Y size of the prediction. + /// public int MinY { get; set; } + /// + /// Gets or sets the maximum X size of the prediction. + /// public int MaxX { get; set; } + /// + /// Gets or sets the maximum Y size of the prediction. + /// public int MaxY { get; set; } - + /// + /// Gets the results of X size. + /// public int SizeX { get @@ -16,7 +39,9 @@ public int SizeX return MaxX - MinX; } } - + /// + /// Gets the results of Y size. + /// public int SizeY { get diff --git a/SynoAI/Models/DTOs/CameraOptionsDto.cs b/SynoAI/Models/DTOs/CameraOptionsDto.cs index 812e3cb..ff9ec1b 100644 --- a/SynoAI/Models/DTOs/CameraOptionsDto.cs +++ b/SynoAI/Models/DTOs/CameraOptionsDto.cs @@ -1,7 +1,13 @@ namespace SynoAI.Models.DTOs { + /// + /// Class for the Camera Options + /// public class CameraOptionsDto : UpdateDto { + /// + /// Boolean for camera enablement + /// public bool Enabled { get diff --git a/SynoAI/Models/DTOs/UpdateDto.cs b/SynoAI/Models/DTOs/UpdateDto.cs index 2846ecd..516ebd9 100644 --- a/SynoAI/Models/DTOs/UpdateDto.cs +++ b/SynoAI/Models/DTOs/UpdateDto.cs @@ -3,15 +3,25 @@ namespace SynoAI.Models.DTOs { + /// + /// Class for update DTOs + /// public abstract class UpdateDto { - public List ChangedProperties = new List(); - + /// + /// List for ChangedProperties + /// + public List ChangedProperties = new(); + /// + /// Notify on change of properties + /// protected void NotifyPropertyChange([CallerMemberName] string propertyName = "") { ChangedProperties.Add(propertyName); } - + /// + /// Boolean to indicate something has changed + /// public bool HasChanged(Expression> expression) { if (expression.Body is not MemberExpression body) @@ -20,7 +30,9 @@ public bool HasChanged(Expression> expression) } return HasChanged(body.Member.Name); } - + /// + /// Boolean to indicate something with changed with the property that changed + /// public bool HasChanged(string propertyName) { return ChangedProperties.Contains(propertyName); diff --git a/SynoAI/Models/Graph.cs b/SynoAI/Models/Graph.cs index 1e814a2..c8bc8e7 100644 --- a/SynoAI/Models/Graph.cs +++ b/SynoAI/Models/Graph.cs @@ -36,7 +36,7 @@ public class GraphData /// /// Highest count of objects / snapshots, used to build the y-Axis labels and graph scaling /// - public int yMax { get; set; } + public int YMax { get; set; } /// /// The amount of time in hours @@ -64,13 +64,16 @@ public class GraphData /// public List GraphPoints { get; set; } + /// + /// Represents a graph data structure. + /// public GraphData() { GraphPoints = new List(); HoursCounter = 0; MinutesCounter = 0; Snapshots = 0; Storage =0; - yMax = 0; + YMax = 0; } @@ -132,9 +135,9 @@ public int GraphYStepSize() public int GraphColsWidth(int graphpoints, bool half = true) { int width = GraphWidth - GraphYAxisWidth; - width = width / graphpoints; - width = width / 2; - if (!half) width = width * 2; + width /= graphpoints; + width /= 2; + if (!half) width *= 2; return width; } @@ -154,16 +157,16 @@ public int GraphBarHeight(int yMax, int value) /// /// Since Y axis shows 'NumberOfSteps' reference values, if there are less than GraphYSteps snapshots, we need to adjust way of displaying the y-axis ref /// - public String yStepping(int yMax, int Step) + public String YStepping(int yMax, int Step) { double yValue = yMax; - yValue = yValue / GraphYSteps; + yValue /= GraphYSteps; yValue = Math.Round(yValue); //Y axis label results being a minimal reasonable number, or it is just the first step (top number, max value) so use it! if (yValue > 1 || Step == 1) { - yValue = yValue * (Step -1); + yValue *= (Step -1); yValue = yMax - yValue; return yValue.ToString(); } diff --git a/SynoAI/Models/OverlapMode.cs b/SynoAI/Models/OverlapMode.cs index a4b9402..18c49d3 100644 --- a/SynoAI/Models/OverlapMode.cs +++ b/SynoAI/Models/OverlapMode.cs @@ -1,5 +1,8 @@ namespace SynoAI.Models { + /// + /// Defines the overlapMode for camera images + /// public enum OverlapMode { /// diff --git a/SynoAI/Models/Point.cs b/SynoAI/Models/Point.cs index f6bd31d..5548479 100644 --- a/SynoAI/Models/Point.cs +++ b/SynoAI/Models/Point.cs @@ -1,8 +1,17 @@ namespace SynoAI.Models { + /// + /// Class to define where the object detection took place + /// public class Point { + /// + /// Gets or sets the X-as. + /// public int X { get; set; } + /// + /// Gets or sets the Y-as. + /// public int Y { get; set; } } } \ No newline at end of file diff --git a/SynoAI/Models/SynologyApiInfo.cs b/SynoAI/Models/SynologyApiInfo.cs index 05ae05d..44e3116 100644 --- a/SynoAI/Models/SynologyApiInfo.cs +++ b/SynoAI/Models/SynologyApiInfo.cs @@ -1,10 +1,25 @@ namespace SynoAI.Models { + /// + /// Represents the API Info from Synology + /// public class SynologyApiInfo { + /// + /// Gets or sets the maximum version of the API + /// public int MaxVersion { get; set; } + /// + /// Gets or sets the minimum version of the API + /// public int MinVersion { get; set; } + /// + /// Gets or sets the path of the API + /// public string Path { get; set; } + /// + /// Gets or sets the requested format of the api + /// public string RequestFormat { get; set; } } } \ No newline at end of file diff --git a/SynoAI/Models/SynologyCamera.cs b/SynoAI/Models/SynologyCamera.cs index 8677961..6ed8507 100644 --- a/SynoAI/Models/SynologyCamera.cs +++ b/SynoAI/Models/SynologyCamera.cs @@ -2,14 +2,28 @@ namespace SynoAI.Models { + /// + /// Class to get the Synology Camera + /// public class SynologyCamera { + /// + /// Gets or sets camera ID + /// public int Id { get; set; } + /// + /// Gets or sets the Name. + /// [JsonProperty("Name")] public string NameOld { get; set; } + /// + /// Gets or sets the NameOld. + /// [JsonProperty("newName")] public string NameNew { get; set; } - + /// + /// Gets or sets the NameNew. + /// public string GetName() { return string.IsNullOrWhiteSpace(NameNew) ? NameOld : NameNew; diff --git a/SynoAI/Models/Zone.cs b/SynoAI/Models/Zone.cs index 4cc13b9..0f3205f 100644 --- a/SynoAI/Models/Zone.cs +++ b/SynoAI/Models/Zone.cs @@ -1,9 +1,21 @@ namespace SynoAI.Models { + /// + /// Represents the Zone + /// public class Zone { + /// + /// Gets or sets the start for the exclusion zone. + /// public Point Start { get; set; } + /// + /// Gets or sets the end for the exclusion zone. + /// public Point End { get; set; } + /// + /// Gets or sets the overlap mode for the exclusion zone. + /// public OverlapMode Mode { get; set; } } } \ No newline at end of file diff --git a/SynoAI/Notifiers/NotifierBase.cs b/SynoAI/Notifiers/NotifierBase.cs index 1e2503b..ca56728 100644 --- a/SynoAI/Notifiers/NotifierBase.cs +++ b/SynoAI/Notifiers/NotifierBase.cs @@ -63,7 +63,7 @@ protected static string GetImageUrl(Camera camera, Notification notification) return null; } - UriBuilder builder = new UriBuilder(Config.SynoAIUrL); + UriBuilder builder = new(Config.SynoAIUrL); builder.Path += $"{camera.Name}/{notification.ProcessedImage.FileName}"; return builder.Uri.ToString(); @@ -82,8 +82,8 @@ protected static string GenerateJSON(Camera camera, Notification notification, b List validPredictions = notification.ValidPredictions.ToList(); jsonObject.predictions = validPredictions.Select(prediction => new { - Confidence = prediction.Confidence, - Label = prediction.Label, + prediction.Confidence, + prediction.Label, // Add other properties as needed }).ToList(); diff --git a/SynoAI/Notifiers/NotifierFactory.cs b/SynoAI/Notifiers/NotifierFactory.cs index 9cccf0e..3589467 100644 --- a/SynoAI/Notifiers/NotifierFactory.cs +++ b/SynoAI/Notifiers/NotifierFactory.cs @@ -18,37 +18,18 @@ internal abstract class NotifierFactory public static INotifier Create(NotifierType type, ILogger logger, IConfigurationSection section) { - NotifierFactory factory; - switch (type) + NotifierFactory factory = type switch { - case NotifierType.Email: - factory = new EmailFactory(); - break; - case NotifierType.Pushbullet: - factory = new PushbulletFactory(); - break; - case NotifierType.Pushover: - factory = new PushoverFactory(); - break; - case NotifierType.SynologyChat: - factory = new SynologyChatFactory(); - break; - case NotifierType.Telegram: - factory = new TelegramFactory(); - break; - case NotifierType.Webhook: - factory = new WebhookFactory(); - break; - case NotifierType.Discord: - factory = new DiscordFactory(); - break; - case NotifierType.MQTT: - factory = new MqttFactory(); - break; - default: - throw new NotImplementedException(type.ToString()); - } - + NotifierType.Email => new EmailFactory(), + NotifierType.Pushbullet => new PushbulletFactory(), + NotifierType.Pushover => new PushoverFactory(), + NotifierType.SynologyChat => new SynologyChatFactory(), + NotifierType.Telegram => new TelegramFactory(), + NotifierType.Webhook => new WebhookFactory(), + NotifierType.Discord => new DiscordFactory(), + NotifierType.MQTT => new MqttFactory(), + _ => throw new NotImplementedException(type.ToString()), + }; INotifier notifier = factory.Create(logger, section); notifier.Cameras = section.GetSection("Cameras").Get>(); notifier.Types = section.GetSection("Types").Get>(); diff --git a/SynoAI/Notifiers/Pushbullet/PushbulletUploadRequest.cs b/SynoAI/Notifiers/Pushbullet/PushbulletUploadRequest.cs index d566228..6b3f789 100644 --- a/SynoAI/Notifiers/Pushbullet/PushbulletUploadRequest.cs +++ b/SynoAI/Notifiers/Pushbullet/PushbulletUploadRequest.cs @@ -2,10 +2,19 @@ namespace SynoAI.Notifiers.Pushbullet { + /// + /// Represents the PusbBullet Upload Request + /// public class PushbulletUploadRequest { + /// + /// Gets or sets the FileName + /// [JsonProperty("file_name")] public string FileName { get; set; } + /// + /// Gets or sets the filetype. + /// [JsonProperty("file_type")] public string FileType { get; set; } } diff --git a/SynoAI/Notifiers/Pushbullet/PushbulletUploadRequestResponse.cs b/SynoAI/Notifiers/Pushbullet/PushbulletUploadRequestResponse.cs index b6ea7a5..060d875 100644 --- a/SynoAI/Notifiers/Pushbullet/PushbulletUploadRequestResponse.cs +++ b/SynoAI/Notifiers/Pushbullet/PushbulletUploadRequestResponse.cs @@ -2,14 +2,29 @@ namespace SynoAI.Notifiers.Pushbullet { + /// + /// Class for uploading the response to PushBUllet + /// public class PushbulletUploadRequestResponse { + /// + /// Gets or sets the FileName. + /// [JsonProperty("file_name")] public string FileName { get; set; } + /// + /// Gets or sets the FileType. + /// [JsonProperty("file_type")] public string FileType { get; set; } + /// + /// Gets or sets the FileUrl. + /// [JsonProperty("file_url")] public string FileUrl { get; set; } + /// + /// Gets or sets the UploadUrl. + /// [JsonProperty("upload_url")] public string UploadUrl { get; set; } } diff --git a/SynoAI/Notifiers/Pushover/Pushover.cs b/SynoAI/Notifiers/Pushover/Pushover.cs index c2622ae..6c67810 100644 --- a/SynoAI/Notifiers/Pushover/Pushover.cs +++ b/SynoAI/Notifiers/Pushover/Pushover.cs @@ -74,34 +74,32 @@ public override async Task SendAsync(Camera camera, Notification notification, I }; // Send the message - using (FileStream imageStream = notification.ProcessedImage.GetReadonlyStream()) - using (StreamContent imageContent = new(imageStream)) - { - imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/png"); - form.Add(imageContent, "attachment", "image.png"); + using FileStream imageStream = notification.ProcessedImage.GetReadonlyStream(); + using StreamContent imageContent = new(imageStream); + imageContent.Headers.ContentType = MediaTypeHeaderValue.Parse("image/png"); + form.Add(imageContent, "attachment", "image.png"); - // Remove content type that is not in the docs - foreach (var param in form) - { - param.Headers.ContentType = null; - } + // Remove content type that is not in the docs + foreach (var param in form) + { + param.Headers.ContentType = null; + } - logger.LogInformation("{cameraName}: Pushover: Sending message", + logger.LogInformation("{cameraName}: Pushover: Sending message", + camera.Name); + HttpResponseMessage responseMessage = await Shared.HttpClient.PostAsync(URI_MESSAGE, form); + if (responseMessage.IsSuccessStatusCode) + { + logger.LogInformation("{cameraName}: Pushover: Notification sent successfully", camera.Name); - HttpResponseMessage responseMessage = await Shared.HttpClient.PostAsync(URI_MESSAGE, form); - if (responseMessage.IsSuccessStatusCode) - { - logger.LogInformation("{cameraName}: Pushover: Notification sent successfully", - camera.Name); - } - else - { - string error = await responseMessage.Content.ReadAsStringAsync(); - logger.LogError("{cameraName}: Pushover: The end point responded with HTTP status code '{responseMessageStatusCode}' and error '{error}'.", - camera.Name, - responseMessage.StatusCode, - error); - } + } + else + { + string error = await responseMessage.Content.ReadAsStringAsync(); + logger.LogError("{cameraName}: Pushover: The end point responded with HTTP status code '{responseMessageStatusCode}' and error '{error}'.", + camera.Name, + responseMessage.StatusCode, + error); } } } diff --git a/SynoAI/Services/IAIService.cs b/SynoAI/Services/IAIService.cs index 9deeba7..4f51631 100644 --- a/SynoAI/Services/IAIService.cs +++ b/SynoAI/Services/IAIService.cs @@ -2,8 +2,18 @@ namespace SynoAI.Services { + /// + /// Represents the AI service interface + /// public interface IAIService { + /// + /// Asynchronously processes an image from the specified camera. + /// + /// The camera from which the image was captured. + /// The image data to be processed. + /// A task that represents the asynchronous operation. + /// The task result contains an enumerable collection of AI predictions. Task> ProcessAsync(Camera camera, byte[] image); } } diff --git a/SynoAI/Services/ISynologyService.cs b/SynoAI/Services/ISynologyService.cs index 3441db3..a3a5283 100644 --- a/SynoAI/Services/ISynologyService.cs +++ b/SynoAI/Services/ISynologyService.cs @@ -3,11 +3,26 @@ namespace SynoAI.Services { + /// + /// Interface for interacting with the Synology service. + /// public interface ISynologyService { + /// + /// Task to initialise the service + /// Task InitialiseAsync(); + /// + /// Logon to the service + /// Task LoginAsync(); + /// + /// Get the cameras available in Synology + /// Task> GetCamerasAsync(); + /// + /// taking the snapshot + /// Task TakeSnapshotAsync(string cameraName); } } diff --git a/SynoAI/Startup.cs b/SynoAI/Startup.cs index b04d624..d297ee7 100644 --- a/SynoAI/Startup.cs +++ b/SynoAI/Startup.cs @@ -3,10 +3,15 @@ using SynoAI.Hubs; namespace SynoAI -{ +{ + /// + /// Configures the services for the application. + /// public class Startup { - // This method gets called by the runtime. Use this method to add services to the container. + /// + /// Configures the services for the application. + /// public void ConfigureServices(IServiceCollection services) { services.AddScoped(); @@ -24,7 +29,9 @@ public void ConfigureServices(IServiceCollection services) services.AddSignalR(); } - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + /// + /// Configures the application. + /// public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IConfiguration configuration, IHostApplicationLifetime lifetime, ILogger logger, ISynologyService synologyService) { Config.Generate(logger, configuration); @@ -54,15 +61,17 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IConfigu lifetime.ApplicationStarted.Register(() => { - List initializationTasks = new List(); - initializationTasks.Add(synologyService.InitialiseAsync()); + List initializationTasks = new() + { + synologyService.InitialiseAsync() + }; initializationTasks.AddRange(Config.Notifiers.Select(n => n.InitializeAsync(logger))); Task.WhenAll(initializationTasks).Wait(); }); lifetime.ApplicationStopping.Register(() => { - List cleanupTasks = new List(); + List cleanupTasks = new(); cleanupTasks.AddRange(Config.Notifiers.Select(n => n.CleanupAsync(logger))); Task.WhenAll(cleanupTasks).Wait(); }); diff --git a/SynoAI/Views/Home/Hour.cshtml b/SynoAI/Views/Home/Hour.cshtml index cec73b7..8244dd0 100644 --- a/SynoAI/Views/Home/Hour.cshtml +++ b/SynoAI/Views/Home/Hour.cshtml @@ -38,14 +38,14 @@
@for (int y = 1; y <= @graphDraw.GraphYSteps; y++) { -
@graphDraw.yStepping(@data.yMax, y)
+
@graphDraw.YStepping(@data.YMax, y)
}
@foreach (SynoAI.Models.GraphPoint graphPoint in data.GraphPoints) { -
-
+
+
}
diff --git a/SynoAI/Views/Home/Index.cshtml b/SynoAI/Views/Home/Index.cshtml index 9348acf..29d527e 100644 --- a/SynoAI/Views/Home/Index.cshtml +++ b/SynoAI/Views/Home/Index.cshtml @@ -41,14 +41,14 @@
@for (int y = 1; y <= @graphDraw.GraphYSteps; y++) { -
@graphDraw.yStepping(@data.yMax, y)
+
@graphDraw.YStepping(@data.YMax, y)
}
@foreach (SynoAI.Models.GraphPoint graphPoint in data.GraphPoints) { -
-
+
+
}