diff --git a/NucuCar.Sensors/Abstractions/GenericTelemeterSensor.cs b/NucuCar.Sensors/Abstractions/GenericTelemeterSensor.cs index 7506e0b..462ab12 100644 --- a/NucuCar.Sensors/Abstractions/GenericTelemeterSensor.cs +++ b/NucuCar.Sensors/Abstractions/GenericTelemeterSensor.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using Newtonsoft.Json.Linq; using NucuCar.Telemetry.Abstractions; namespace NucuCar.Sensors.Abstractions @@ -13,7 +13,7 @@ namespace NucuCar.Sensors.Abstractions { protected bool TelemetryEnabled; public abstract string GetIdentifier(); - public abstract Dictionary GetTelemetryJson(); + public abstract JObject GetTelemetryJson(); public abstract bool IsTelemetryEnabled(); } } \ No newline at end of file diff --git a/NucuCar.Sensors/Modules/BME680/Bme680Sensor.cs b/NucuCar.Sensors/Modules/BME680/Bme680Sensor.cs index 4ec1f64..7cf3d1a 100644 --- a/NucuCar.Sensors/Modules/BME680/Bme680Sensor.cs +++ b/NucuCar.Sensors/Modules/BME680/Bme680Sensor.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Iot.Device.Bmxx80; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; using NucuCar.Sensors.Abstractions; using UnitsNet; using Bme680 = Iot.Device.Bmxx80.Bme680; @@ -153,14 +154,14 @@ namespace NucuCar.Sensors.Modules.BME680 return "Environment"; } - public override Dictionary GetTelemetryJson() + public override JObject GetTelemetryJson() { - Dictionary returnValue = null; + JObject jsonObject = null; if (_lastMeasurement != null && TelemetryEnabled) { - returnValue = new Dictionary + jsonObject = new JObject() { - ["sensor_state"] = CurrentState, + ["sensor_state"] = GetState().ToString(), ["temperature"] = _lastMeasurement.Temperature, ["humidity"] = _lastMeasurement.Humidity, ["pressure"] = _lastMeasurement.Pressure, @@ -168,7 +169,7 @@ namespace NucuCar.Sensors.Modules.BME680 }; } - return returnValue; + return jsonObject; } public override bool IsTelemetryEnabled() diff --git a/NucuCar.Sensors/Modules/CpuTemperature/CpuTempSensor.cs b/NucuCar.Sensors/Modules/CpuTemperature/CpuTempSensor.cs index a8f65c9..dbd1745 100644 --- a/NucuCar.Sensors/Modules/CpuTemperature/CpuTempSensor.cs +++ b/NucuCar.Sensors/Modules/CpuTemperature/CpuTempSensor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; using NucuCar.Sensors.Abstractions; namespace NucuCar.Sensors.Modules.CpuTemperature @@ -84,14 +85,14 @@ namespace NucuCar.Sensors.Modules.CpuTemperature return "CpuTemperature"; } - public override Dictionary GetTelemetryJson() + public override JObject GetTelemetryJson() { - Dictionary returnValue = null; + JObject returnValue = null; if (!double.IsNaN(_lastTemperatureCelsius) && TelemetryEnabled) { - returnValue = new Dictionary + returnValue = new JObject { - ["sensor_state"] = CurrentState, + ["sensor_state"] = GetState().ToString(), ["cpu_temperature"] = _lastTemperatureCelsius, }; } diff --git a/NucuCar.Sensors/Modules/Heartbeat/HeartbeatSensor.cs b/NucuCar.Sensors/Modules/Heartbeat/HeartbeatSensor.cs index 087d40d..00a8fe4 100644 --- a/NucuCar.Sensors/Modules/Heartbeat/HeartbeatSensor.cs +++ b/NucuCar.Sensors/Modules/Heartbeat/HeartbeatSensor.cs @@ -1,7 +1,9 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; using NucuCar.Sensors.Abstractions; namespace NucuCar.Sensors.Modules.Heartbeat @@ -59,15 +61,17 @@ namespace NucuCar.Sensors.Modules.Heartbeat return "Heartbeat"; } - public override Dictionary GetTelemetryJson() + public override JObject GetTelemetryJson() { - var returnValue = new Dictionary + if (TelemetryEnabled) + { + return new JObject { - ["sensor_state"] = CurrentState, - ["value"] = 1, + ["sensor_state"] = GetState().ToString(), + ["last_seen"] = DateTime.UtcNow, }; - - return returnValue; + } + return null; } public override bool IsTelemetryEnabled() diff --git a/NucuCar.Sensors/Modules/PMS5003/Pms5003Sensor.cs b/NucuCar.Sensors/Modules/PMS5003/Pms5003Sensor.cs index 2a0f142..07054a9 100644 --- a/NucuCar.Sensors/Modules/PMS5003/Pms5003Sensor.cs +++ b/NucuCar.Sensors/Modules/PMS5003/Pms5003Sensor.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Newtonsoft.Json.Linq; using NucuCar.Sensors.Abstractions; using PMS5003; using PMS5003.Exceptions; @@ -124,15 +125,15 @@ namespace NucuCar.Sensors.Modules.PMS5003 return "Pms5003"; } - public override Dictionary GetTelemetryJson() + public override JObject GetTelemetryJson() { - Dictionary returnValue = null; + JObject returnValue = null; if (_pms5003Data != null && TelemetryEnabled) { // The telemetry handled by FirebaseRestTranslator wants the values to be int or double. - returnValue = new Dictionary + returnValue = new JObject() { - ["sensor_state"] = GetState(), + ["sensor_state"] = GetState().ToString(), ["Pm1Atmospheric"] = _pms5003Data.Pm1Atmospheric, ["Pm1Standard"] = _pms5003Data.Pm1Standard, ["Pm10Atmospheric"] = _pms5003Data.Pm10Atmospheric, diff --git a/NucuCar.Sensors/Program.cs b/NucuCar.Sensors/Program.cs index 2ba0c21..b5b3d27 100644 --- a/NucuCar.Sensors/Program.cs +++ b/NucuCar.Sensors/Program.cs @@ -7,6 +7,7 @@ using NucuCar.Sensors.Modules.Heartbeat; using NucuCar.Sensors.Modules.PMS5003; using NucuCar.Telemetry; using NucuCar.Telemetry.Abstractions; +using NucuCar.Telemetry.Publishers; namespace NucuCar.Sensors { @@ -21,14 +22,14 @@ namespace NucuCar.Sensors Host.CreateDefaultBuilder(args) .ConfigureServices((hostContext, services) => { - services.Configure(hostContext.Configuration.GetSection("Telemetry")); + services.Configure(hostContext.Configuration.GetSection("Telemetry")); services.Configure(hostContext.Configuration.GetSection("Bme680Sensor")); services.Configure(hostContext.Configuration.GetSection("CpuTemperatureSensor")); services.Configure(hostContext.Configuration.GetSection("HeartbeatSensor")); services.Configure(hostContext.Configuration.GetSection("Pms5003Sensor")); // Singletons - services.AddSingleton(); + services.AddSingleton(); services.AddSingleton, Bme680Sensor>(); services.AddSingleton, CpuTempSensor>(); services.AddSingleton, HeartbeatSensor>(); diff --git a/NucuCar.Sensors/appsettings.json b/NucuCar.Sensors/appsettings.json index f018d1b..df706ec 100644 --- a/NucuCar.Sensors/appsettings.json +++ b/NucuCar.Sensors/appsettings.json @@ -14,7 +14,7 @@ "Telemetry": true }, "HeartbeatSensor": { - "Enabled": false, + "Enabled": true, "Telemetry": true }, "Pms5003Sensor": { diff --git a/NucuCar.Telemetry/Abstractions/ITelemeter.cs b/NucuCar.Telemetry/Abstractions/ITelemeter.cs index 7864de1..b276e3b 100644 --- a/NucuCar.Telemetry/Abstractions/ITelemeter.cs +++ b/NucuCar.Telemetry/Abstractions/ITelemeter.cs @@ -1,10 +1,11 @@ -using System.Collections.Generic; +using Newtonsoft.Json.Linq; +using NucuCar.Telemetry.Publishers; namespace NucuCar.Telemetry.Abstractions { /// /// Interface that specifies that the component implementing it is willing to provide telemetry data and can be - /// registered to a publisher such as . + /// registered to a publisher such as . /// public interface ITelemeter { @@ -14,14 +15,13 @@ namespace NucuCar.Telemetry.Abstractions /// An identifier for the telemetry source. string GetIdentifier(); - // TODO: Perhaps here it's better if we return a string or a json object from Newtonsoft. /// /// This function should return a dictionary containing the telemetry data. /// When implementing this function you should return null if the telemetry is disabled. /// See: /// - /// The telemetry data. It should be JSON serializable. - Dictionary GetTelemetryJson(); + /// The telemetry data as a Newtonsoft JObject. + JObject GetTelemetryJson(); /// /// This function should return whether the sensor has telemetry enabled or not. diff --git a/NucuCar.Telemetry/TelemetryConfig.cs b/NucuCar.Telemetry/Config.cs similarity index 54% rename from NucuCar.Telemetry/TelemetryConfig.cs rename to NucuCar.Telemetry/Config.cs index a60bc5d..304aef1 100644 --- a/NucuCar.Telemetry/TelemetryConfig.cs +++ b/NucuCar.Telemetry/Config.cs @@ -1,14 +1,14 @@ // ReSharper disable UnusedAutoPropertyAccessor.Global -using NucuCar.Telemetry.Abstractions; +using NucuCar.Telemetry.Publishers; namespace NucuCar.Telemetry { - public class TelemetryConfig + public class Config { /// - /// The Publisher is used by to instantiate - /// the correct . For available types see + /// The Publisher is used by to instantiate + /// the correct . For available types see /// public string Publisher { get; set; } diff --git a/NucuCar.Telemetry/DataAggregate.cs b/NucuCar.Telemetry/DataAggregate.cs new file mode 100644 index 0000000..74f305d --- /dev/null +++ b/NucuCar.Telemetry/DataAggregate.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json.Linq; + +// ReSharper disable UnusedAutoPropertyAccessor.Global +// ReSharper disable AutoPropertyCanBeMadeGetOnly.Global + +namespace NucuCar.Telemetry +{ + /** + * is an entity holding the telemetry data body. + * It contains the telemetry data from all the telemeters. + */ + public class DataAggregate + { + public string Source { get; set; } + public DateTime Timestamp { get; set; } + public List Data { get; set; } + + public DataAggregate(string source, List data) + { + Source = source; + Data = data; + Timestamp = DateTime.UtcNow; + } + } +} \ No newline at end of file diff --git a/NucuCar.Telemetry/TelemetryPublisherFactory.cs b/NucuCar.Telemetry/PublisherFactory.cs similarity index 66% rename from NucuCar.Telemetry/TelemetryPublisherFactory.cs rename to NucuCar.Telemetry/PublisherFactory.cs index f0b5cb0..250304b 100644 --- a/NucuCar.Telemetry/TelemetryPublisherFactory.cs +++ b/NucuCar.Telemetry/PublisherFactory.cs @@ -3,55 +3,55 @@ using Microsoft.Extensions.Logging; using NucuCar.Core.Utilities; using NucuCar.Telemetry.Abstractions; using NucuCar.Telemetry.Publishers; +using Console = NucuCar.Telemetry.Publishers.Console; namespace NucuCar.Telemetry { /// - /// The TelemetryPublisherFactory is used instantiate TelemetryPublishers. + /// The PublisherFactory is used instantiate TelemetryPublishers. /// - public static class TelemetryPublisherFactory + public static class PublisherFactory { /// - /// Creates an instance of . See + /// Creates an instance of . See /// - /// The type of the publisher. + /// The type of the publisher. /// Device connection string for the telemetry publisher. /// String that is used to identify the source of the telemetry data. /// An logger instance. - /// A instance. + /// A instance. public static ITelemetryPublisher Create(string type, string connectionString, string telemetrySource, ILogger logger) { Guard.ArgumentNotNullOrWhiteSpace(nameof(connectionString), connectionString); Guard.ArgumentNotNullOrWhiteSpace(nameof(telemetrySource), telemetrySource); Guard.ArgumentNotNull(nameof(logger), logger); - var opts = new TelemetryPublisherOptions + var opts = new PublisherOptions {ConnectionString = connectionString, TelemetrySource = telemetrySource, Logger = logger}; return SpawnPublisher(type, opts); } /// - /// Creates an instance of . + /// Creates an instance of . /// - /// The type of the publisher. See + /// The type of the publisher. See /// The device connection string for the selected publisher. - /// A instance. + /// A instance. public static ITelemetryPublisher CreateFromConnectionString(string type, string connectionString) { Guard.ArgumentNotNullOrWhiteSpace(nameof(connectionString), connectionString); - var opts = new TelemetryPublisherOptions() + var opts = new PublisherOptions() {ConnectionString = connectionString, TelemetrySource = "NucuCar.Sensors"}; return SpawnPublisher(type, opts); } - private static ITelemetryPublisher SpawnPublisher(string type, TelemetryPublisherOptions opts) + private static ITelemetryPublisher SpawnPublisher(string type, PublisherOptions opts) { return type switch { - TelemetryPublisherType.Azure => new TelemetryPublisherAzure(opts), - TelemetryPublisherType.Disk => new TelemetryPublisherDisk(opts), - TelemetryPublisherType.Firestore => new TelemetryPublisherFirestore(opts), - TelemetryPublisherType.Console => new TelemetryPublisherConsole(opts), + PublisherType.Azure => new Azure(opts), + PublisherType.Disk => new Disk(opts), + PublisherType.Console => new Console(opts), _ => throw new ArgumentException($"Invalid TelemetryPublisher type: {type}.") }; } diff --git a/NucuCar.Telemetry/TelemetryPublisherOptions.cs b/NucuCar.Telemetry/PublisherOptions.cs similarity index 79% rename from NucuCar.Telemetry/TelemetryPublisherOptions.cs rename to NucuCar.Telemetry/PublisherOptions.cs index 492a0fc..8ef1895 100644 --- a/NucuCar.Telemetry/TelemetryPublisherOptions.cs +++ b/NucuCar.Telemetry/PublisherOptions.cs @@ -1,12 +1,12 @@ using Microsoft.Extensions.Logging; -using NucuCar.Telemetry.Abstractions; +using NucuCar.Telemetry.Publishers; namespace NucuCar.Telemetry { /// - /// This class contains options for the . + /// This class contains options for the . /// - public class TelemetryPublisherOptions + public class PublisherOptions { /// /// The ConnectionString used by the publisher to connect to the cloud service. diff --git a/NucuCar.Telemetry/TelemetryPublisherProxy.cs b/NucuCar.Telemetry/PublisherProxy.cs similarity index 78% rename from NucuCar.Telemetry/TelemetryPublisherProxy.cs rename to NucuCar.Telemetry/PublisherProxy.cs index 86b890f..e9b6d40 100644 --- a/NucuCar.Telemetry/TelemetryPublisherProxy.cs +++ b/NucuCar.Telemetry/PublisherProxy.cs @@ -7,7 +7,7 @@ using NucuCar.Telemetry.Abstractions; // ReSharper disable ClassWithVirtualMembersNeverInherited.Global namespace NucuCar.Telemetry { - public class TelemetryPublisherProxy : ITelemetryPublisher + public class PublisherProxy : ITelemetryPublisher { // TODO: Add support for chaining publishers. private ITelemetryPublisher Publisher { get; } @@ -16,15 +16,15 @@ namespace NucuCar.Telemetry /// Class used together with the DI, holds a Publisher instance that's being create by options from /// TelemetryConfig. /// - public TelemetryPublisherProxy() + public PublisherProxy() { } - public TelemetryPublisherProxy(ILogger logger, IOptions options) + public PublisherProxy(ILogger logger, IOptions options) { if (options.Value.ServiceEnabled) { - Publisher = TelemetryPublisherFactory.Create(options.Value.Publisher, options.Value.ConnectionString, + Publisher = PublisherFactory.Create(options.Value.Publisher, options.Value.ConnectionString, "NucuCar.Sensors", logger); } else diff --git a/NucuCar.Telemetry/TelemetryPublisherType.cs b/NucuCar.Telemetry/PublisherType.cs similarity index 73% rename from NucuCar.Telemetry/TelemetryPublisherType.cs rename to NucuCar.Telemetry/PublisherType.cs index 8af6f2b..219fb64 100644 --- a/NucuCar.Telemetry/TelemetryPublisherType.cs +++ b/NucuCar.Telemetry/PublisherType.cs @@ -1,11 +1,11 @@ -using NucuCar.Telemetry.Abstractions; +using NucuCar.Telemetry.Publishers; namespace NucuCar.Telemetry { /// - /// TelemetryPublisherType holds constants for instantiating , + /// TelemetryPublisherType holds constants for instantiating , /// - public static class TelemetryPublisherType + public static class PublisherType { public const string Azure = "Azure"; public const string Disk = "Disk"; diff --git a/NucuCar.Telemetry/Publishers/TelemetryPublisherAzure.cs b/NucuCar.Telemetry/Publishers/Azure.cs similarity index 89% rename from NucuCar.Telemetry/Publishers/TelemetryPublisherAzure.cs rename to NucuCar.Telemetry/Publishers/Azure.cs index 853e1f1..c7e9613 100644 --- a/NucuCar.Telemetry/Publishers/TelemetryPublisherAzure.cs +++ b/NucuCar.Telemetry/Publishers/Azure.cs @@ -5,22 +5,21 @@ using System.Threading.Tasks; using Microsoft.Azure.Devices.Client; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using NucuCar.Telemetry.Abstractions; namespace NucuCar.Telemetry.Publishers { /// - /// Constructs an instance of . It is used to publish telemetry to Microsoft + /// Constructs an instance of . It is used to publish telemetry to Microsoft /// Azure IotHub /// /// The connection string can be found in your Azure IotHub. /// /// - public class TelemetryPublisherAzure : TelemetryPublisher + public class Azure : BasePublisher { protected readonly DeviceClient DeviceClient; - public TelemetryPublisherAzure(TelemetryPublisherOptions opts) : base(opts) + public Azure(PublisherOptions opts) : base(opts) { try { diff --git a/NucuCar.Telemetry/Abstractions/TelemetryPublisher.cs b/NucuCar.Telemetry/Publishers/BasePublisher.cs similarity index 82% rename from NucuCar.Telemetry/Abstractions/TelemetryPublisher.cs rename to NucuCar.Telemetry/Publishers/BasePublisher.cs index 4d5c40e..272901e 100644 --- a/NucuCar.Telemetry/Abstractions/TelemetryPublisher.cs +++ b/NucuCar.Telemetry/Publishers/BasePublisher.cs @@ -3,13 +3,15 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Newtonsoft.Json.Linq; +using NucuCar.Telemetry.Abstractions; -namespace NucuCar.Telemetry.Abstractions +namespace NucuCar.Telemetry.Publishers { /// /// The TelemetryPublisher is an abstract class, which provides a base for implementing telemetry publishers. /// - public abstract class TelemetryPublisher : IDisposable, ITelemetryPublisher + public abstract class BasePublisher : IDisposable, ITelemetryPublisher { /// /// Raw connection string that is used to connect to the cloud service. Should be parsed if required. @@ -35,16 +37,16 @@ namespace NucuCar.Telemetry.Abstractions /// /// Parameter less constructor, mainly used for testing. /// - public TelemetryPublisher() + public BasePublisher() { RegisteredTelemeters = new List(10); } /// - /// Constructor for . + /// Constructor for . /// - /// TelemetryPublisher options, see: - protected TelemetryPublisher(TelemetryPublisherOptions opts) + /// TelemetryPublisher options, see: + protected BasePublisher(PublisherOptions opts) { ConnectionString = opts.ConnectionString; TelemetrySource = opts.TelemetrySource; @@ -97,10 +99,11 @@ namespace NucuCar.Telemetry.Abstractions /// Iterates through the registered telemeters and returns the telemetry data as dictionary. /// It also adds metadata information such as: source and timestamp. /// - /// A dictionary containing all telemetry data. - protected virtual Dictionary GetTelemetry() + /// A dictionary containing all telemetry data. + protected virtual DataAggregate GetTelemetry() { - var data = new List>(); + var source = TelemetrySource ?? nameof(BasePublisher); + var allTelemetryData = new List(); foreach (var telemeter in RegisteredTelemeters) { var telemetryData = telemeter.GetTelemetryJson(); @@ -110,17 +113,10 @@ namespace NucuCar.Telemetry.Abstractions continue; } - telemetryData["_id"] = telemeter.GetIdentifier(); - data.Add(telemetryData); + telemetryData["sensor_name"] = telemeter.GetIdentifier(); + allTelemetryData.Add(telemetryData); } - - var metadata = new Dictionary - { - ["source"] = TelemetrySource ?? nameof(TelemetryPublisher), - ["timestamp"] = DateTime.UtcNow, - ["data"] = data.ToArray() - }; - return metadata; + return new DataAggregate(source, allTelemetryData); } } } \ No newline at end of file diff --git a/NucuCar.Telemetry/Publishers/TelemetryPublisherConsole.cs b/NucuCar.Telemetry/Publishers/Console.cs similarity index 75% rename from NucuCar.Telemetry/Publishers/TelemetryPublisherConsole.cs rename to NucuCar.Telemetry/Publishers/Console.cs index c35c954..ae8d3dd 100644 --- a/NucuCar.Telemetry/Publishers/TelemetryPublisherConsole.cs +++ b/NucuCar.Telemetry/Publishers/Console.cs @@ -2,14 +2,13 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Newtonsoft.Json; -using NucuCar.Telemetry.Abstractions; namespace NucuCar.Telemetry.Publishers { - public class TelemetryPublisherConsole : TelemetryPublisher + public class Console : BasePublisher { - public TelemetryPublisherConsole(TelemetryPublisherOptions opts) : base(opts) + public Console(PublisherOptions opts) : base(opts) { } diff --git a/NucuCar.Telemetry/Publishers/TelemetryPublisherDisk.cs b/NucuCar.Telemetry/Publishers/Disk.cs similarity index 92% rename from NucuCar.Telemetry/Publishers/TelemetryPublisherDisk.cs rename to NucuCar.Telemetry/Publishers/Disk.cs index 52ddfd7..f8811f7 100644 --- a/NucuCar.Telemetry/Publishers/TelemetryPublisherDisk.cs +++ b/NucuCar.Telemetry/Publishers/Disk.cs @@ -7,20 +7,19 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using NucuCar.Core.Utilities; -using NucuCar.Telemetry.Abstractions; namespace NucuCar.Telemetry.Publishers { /// /// The TelemetryPublisherDisk is used to publish telemetry data to a file on the disk. /// - public class TelemetryPublisherDisk : TelemetryPublisher + public class Disk : BasePublisher { private readonly FileStream _fileStream; private readonly string _separator; /// - /// Constructs an instance of . + /// Constructs an instance of . /// /// The connection string must contain the following options: /// Filename (optional) - The path of the filename in which to log telemetry data. @@ -30,7 +29,7 @@ namespace NucuCar.Telemetry.Publishers /// /// /// - public TelemetryPublisherDisk(TelemetryPublisherOptions opts) : base(opts) + public Disk(PublisherOptions opts) : base(opts) { var connectionStringParams = ConnectionStringParser.Parse(opts.ConnectionString); var fileName = connectionStringParams.GetValueOrDefault("FileName", "telemetry"); diff --git a/NucuCar.Telemetry/Publishers/TelemetryPublisherFirestore.cs b/NucuCar.Telemetry/Publishers/TelemetryPublisherFirestore.cs deleted file mode 100644 index a4f207a..0000000 --- a/NucuCar.Telemetry/Publishers/TelemetryPublisherFirestore.cs +++ /dev/null @@ -1,183 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using NucuCar.Core.Http; -using NucuCar.Core.Utilities; -using NucuCar.Telemetry.Abstractions; - -namespace NucuCar.Telemetry.Publishers -{ - /// - /// This class is used to publish the telemetry data to Google's Cloud Firestore. - /// Requires the environment variable: GOOGLE_APPLICATION_CREDENTIALS to be set. - /// See: https://cloud.google.com/docs/authentication/getting-started - /// or Firebase > Project Settings > Service Accounts (Authentication is not implemented!) - /// - /// The connection string has the following parameters: - /// ProjectId (required) — The string for the Firestore project id. - /// CollectionName (required) — The string for the Firestore collection name. - /// WebApiKey (optional) — The web api key of the firebase project. - /// WebApiEmail (optional) — An email to use when requesting id tokens. - /// WebApiPassword (optional) — The password to use when requesting id tokens. - /// Timeout (optional) — The number in milliseconds in which to timeout if publishing fails. Default: 10000 - /// - /// - public class TelemetryPublisherFirestore : TelemetryPublisher - { - protected MinimalHttpClient HttpClient; - - private string _idToken; - private DateTime _authorizationExpiryTime; - - // Variables used for authentication - private readonly string _webEmail; - private readonly string _webPassword; - private readonly string _webApiKey; - - public TelemetryPublisherFirestore(TelemetryPublisherOptions opts) : base(opts) - { - // Parse Options - var options = ConnectionStringParser.Parse(opts.ConnectionString); - if (!options.TryGetValue("ProjectId", out var firestoreProjectId)) - { - Logger?.LogCritical( - "Can't start {Name}! Malformed connection string! Missing ProjectId!", - nameof(TelemetryPublisherFirestore)); - throw new ArgumentException("Malformed connection string!"); - } - - if (!options.TryGetValue("CollectionName", out var firestoreCollection)) - { - Logger?.LogCritical( - "Can't start {Name}! Malformed connection string! Missing CollectionName!", - nameof(TelemetryPublisherFirestore)); - throw new ArgumentException("Malformed connection string!"); - } - - var timeout = int.Parse(options.GetValueOrDefault("Timeout", "10000") ?? "10000"); - _webApiKey = options.GetValueOrDefault("WebApiKey", null); - _webEmail = options.GetValueOrDefault("WebApiEmail", null); - _webPassword = options.GetValueOrDefault("WebApiPassword", null); - - // Setup HttpClient - var requestUrl = $"https://firestore.googleapis.com/v1/projects/{firestoreProjectId}/" + - $"databases/(default)/documents/{firestoreCollection}/"; - HttpClient = new MinimalHttpClient(requestUrl) {Timeout = timeout, Logger = Logger}; - Logger?.LogInformation("Initialized {Name}", nameof(TelemetryPublisherFirestore)); - Logger?.LogInformation("ProjectId: {FirestoreProjectId}; CollectionName: {FirestoreCollection}", - firestoreProjectId, firestoreCollection); - } - - private async Task SetupAuthorization() - { - HttpClient.ClearAuthorizationHeader(); - - // https://cloud.google.com/identity-platform/docs/use-rest-api#section-sign-in-email-password - var requestUrl = $"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={_webApiKey}"; - var data = new Dictionary() - { - ["email"] = _webEmail, - ["password"] = _webPassword, - ["returnSecureToken"] = true - }; - - var response = await HttpClient.PostAsync(requestUrl, data); - - if (response?.StatusCode == HttpStatusCode.OK) - { - Logger?.LogInformation("Firestore authentication OK!"); - var jsonContent = await response.GetJson(); - _idToken = jsonContent.GetProperty("idToken").ToString(); - // Setup next expire. - var expiresIn = double.Parse(jsonContent.GetProperty("expiresIn").ToString()); - _authorizationExpiryTime = DateTime.UtcNow.AddSeconds(expiresIn); - HttpClient.Authorization(_idToken); - } - else - { - Logger?.LogError("Firestore authentication request failed! {StatusCode}!", response?.StatusCode); - if (response != null) - { - var contentBody = await response.Content.ReadAsStringAsync(); - Logger?.LogDebug("{Body}", contentBody); - } - } - } - - private async Task CheckAndSetupAuthorization() - { - // If there are no credentials or partial credentials supplies there must be no authorization. - if (_webApiKey == null || _webEmail == null || _webPassword == null) - { - return; - } - - // Check if the token is about to expire in the next 15 minutes. - if (DateTime.UtcNow.AddMinutes(15) < _authorizationExpiryTime) - { - return; - } - - await SetupAuthorization(); - } - - public override async Task PublishAsync(CancellationToken cancellationToken) - { - if (cancellationToken.IsCancellationRequested) - { - return; - } - - var data = FirebaseRestTranslator.Translator.Translate(null, GetTelemetry()); - - HttpResponseMessage responseMessage = null; - try - { - await CheckAndSetupAuthorization(); - responseMessage = await HttpClient.PostAsync("", data); - } - // ArgumentException occurs during json serialization errors. - catch (ArgumentException e) - { - Logger?.LogWarning("{Message}", e.Message); - } - - - switch (responseMessage?.StatusCode) - { - case HttpStatusCode.OK: - Logger?.LogInformation("Published data to Firestore!"); - break; - case HttpStatusCode.Forbidden: - case HttpStatusCode.Unauthorized: - { - Logger?.LogError("Failed to publish telemetry data! {StatusCode}. Retrying...", - responseMessage.StatusCode); - await SetupAuthorization(); - responseMessage = await HttpClient.PostAsync("", data); - if (responseMessage != null && responseMessage.IsSuccessStatusCode) - { - Logger?.LogInformation("Published data to Firestore on retry!"); - } - else - { - Logger?.LogError("Failed to publish telemetry data! {StatusCode}", responseMessage?.StatusCode); - } - - break; - } - default: - Logger?.LogError("Failed to publish telemetry data! {StatusCode}", responseMessage?.StatusCode); - break; - } - } - - public override void Dispose() - { - } - } -} \ No newline at end of file diff --git a/NucuCar.Telemetry/Readme.md b/NucuCar.Telemetry/Readme.md index c10553e..b6d97b8 100644 --- a/NucuCar.Telemetry/Readme.md +++ b/NucuCar.Telemetry/Readme.md @@ -69,21 +69,3 @@ See the source code for comments on the ConnectionString. You will need to parse the file by yourself. --- - -## Firebase Firestore Database - -### Publisher - -Publishes telemetry on the firestore. - -The `Telemetry:Publisher` must be set to: Firestore - -Example connection string: -`ProjectId=nucuhub;CollectionName=sensors-telemetry-test;Timeout=1000` - -If you want to use Authentication you can do so by providing the following keys -in the connection string: WebApiEmail, WebApiPassword, WebApiKey. - -### Reader - -You will need use a firebase client or rest API. \ No newline at end of file diff --git a/NucuCar.Telemetry/TelemetryWorker.cs b/NucuCar.Telemetry/TelemetryWorker.cs index 9201fe1..e12f76c 100644 --- a/NucuCar.Telemetry/TelemetryWorker.cs +++ b/NucuCar.Telemetry/TelemetryWorker.cs @@ -18,7 +18,7 @@ namespace NucuCar.Telemetry private readonly ILogger _logger; private readonly ITelemetryPublisher _telemetryPublisher; - public TelemetryWorker(ILogger logger, IOptions options, + public TelemetryWorker(ILogger logger, IOptions options, ITelemetryPublisher telemetryPublisherProxy) { _logger = logger; diff --git a/NucuCar.UnitTests/NucuCar.Telemetry/TelemetryPublisherFactoryTest.cs b/NucuCar.UnitTests/NucuCar.Telemetry/TelemetryPublisherFactoryTest.cs index 92396e4..39e3e4a 100644 --- a/NucuCar.UnitTests/NucuCar.Telemetry/TelemetryPublisherFactoryTest.cs +++ b/NucuCar.UnitTests/NucuCar.Telemetry/TelemetryPublisherFactoryTest.cs @@ -13,8 +13,8 @@ namespace NucuCar.UnitTests.NucuCar.Telemetry const string connectionString = "HostName=something.azure-devices.net;DeviceId=something;SharedAccessKey=test"; var telemetryPublisher = - TelemetryPublisherFactory.CreateFromConnectionString(TelemetryPublisherType.Azure, connectionString); - Assert.IsType(telemetryPublisher); + PublisherFactory.CreateFromConnectionString(PublisherType.Azure, connectionString); + Assert.IsType(telemetryPublisher); } [Fact] @@ -23,26 +23,16 @@ namespace NucuCar.UnitTests.NucuCar.Telemetry const string connectionString = "Filename=test;BufferSize=4096"; var telemetryPublisher = - TelemetryPublisherFactory.CreateFromConnectionString(TelemetryPublisherType.Disk, connectionString); - Assert.IsType(telemetryPublisher); + PublisherFactory.CreateFromConnectionString(PublisherType.Disk, connectionString); + Assert.IsType(telemetryPublisher); } - [Fact] - private void Test_Build_TelemetryPublisherFiresstore() - { - const string connectionString = - "ProjectId=test;CollectionName=test"; - var telemetryPublisher = - TelemetryPublisherFactory.CreateFromConnectionString(TelemetryPublisherType.Firestore, connectionString); - Assert.IsType(telemetryPublisher); - } - [Fact] private void Test_Build_ThrowsOnInvalidType() { Assert.Throws(() => { - TelemetryPublisherFactory.CreateFromConnectionString("_1", "a=b"); + PublisherFactory.CreateFromConnectionString("_1", "a=b"); }); } } diff --git a/NucuCar.UnitTests/NucuCar.Telemetry/TelemetryPublisherFirestoreTest.cs b/NucuCar.UnitTests/NucuCar.Telemetry/TelemetryPublisherFirestoreTest.cs deleted file mode 100644 index 97d4d48..0000000 --- a/NucuCar.UnitTests/NucuCar.Telemetry/TelemetryPublisherFirestoreTest.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using NucuCar.Core.Http; -using NucuCar.Telemetry; -using NucuCar.Telemetry.Publishers; -using Xunit; - -namespace NucuCar.UnitTests.NucuCar.Telemetry -{ - /// - /// Class used to test the TelemetryPublisherFirestore by mocking the GetTelemetry method and HttpClient field. - /// - internal class MockTelemetryPublisherFirestore : TelemetryPublisherFirestore - { - private Dictionary _mockData; - - public MockTelemetryPublisherFirestore(TelemetryPublisherOptions opts) : base(opts) - { - _mockData = new Dictionary(); - } - - public void SetHttpClient(MinimalHttpClient client) - { - HttpClient = client; - } - - public void SetMockData(Dictionary data) - { - _mockData = data; - } - - protected override Dictionary GetTelemetry() - { - return _mockData; - } - } - - public class TelemetryPublisherFirestoreTest - { - [Fact] - private void Test_Construct_BadProjectId() - { - // Setup - var opts = new TelemetryPublisherOptions() - { - ConnectionString = "ProjectIdBAD=test;CollectionName=test" - }; - - // Run & Assert - Assert.Throws(() => { new MockTelemetryPublisherFirestore(opts); }); - } - - [Fact] - private void Test_Construct_BadCollectionName() - { - // Setup - var opts = new TelemetryPublisherOptions() - { - ConnectionString = "ProjectId=test;CollectionNameBAD=test" - }; - - // Run & Assert - Assert.Throws(() => { new MockTelemetryPublisherFirestore(opts); }); - } - - [Fact] - private async Task Test_PublishAsync_OK() - { - // Setup - var opts = new TelemetryPublisherOptions() - { - ConnectionString = "ProjectId=test;CollectionName=test;WebApiKey=TAPIKEY;WebApiEmail=t@emai.com;WebApiPassword=tpass" - }; - var publisher = new MockTelemetryPublisherFirestore(opts); - var mockHttpClient = new MockMinimalHttpClient("http://testing.com"); - var authResponse = new HttpResponseMessage(HttpStatusCode.OK) - {Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"3600\"}")}; - mockHttpClient.SendAsyncResponses.Add(authResponse); - mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK)); - publisher.SetHttpClient(mockHttpClient); - publisher.SetMockData(new Dictionary {["testData"] = 1}); - - // Run - await publisher.PublishAsync(CancellationToken.None); - - // Assert - var request = mockHttpClient.SendAsyncArgCalls[1]; - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(new Uri("http://testing.com"), request.RequestUri); - Assert.Equal("{\"fields\":{\"testData\":{\"integerValue\":1}}}", - request.Content.ReadAsStringAsync().GetAwaiter().GetResult()); - } - - [Fact] - private async Task Test_PublishAsync_InvalidJson() - { - // Setup - var opts = new TelemetryPublisherOptions() - { - ConnectionString = "ProjectId=test;CollectionName=test;WebApiKey=TAPIKEY;WebApiEmail=t@emai.com;WebApiPassword=tpass" - }; - var publisher = new MockTelemetryPublisherFirestore(opts); - var mockHttpClient = new MockMinimalHttpClient("http://testing.com"); - var authResponse = new HttpResponseMessage(HttpStatusCode.OK) - {Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"3600\"}")}; - mockHttpClient.SendAsyncResponses.Add(authResponse); - mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK)); - publisher.SetHttpClient(mockHttpClient); - publisher.SetMockData(new Dictionary {["testData"] = double.PositiveInfinity}); - - // Run - await publisher.PublishAsync(CancellationToken.None); - - // Assert only auth request made. - Assert.Single(mockHttpClient.SendAsyncArgCalls); - } - - [Fact] - private async Task Test_PublishAsync_Cancel() - { - // Setup - var opts = new TelemetryPublisherOptions() - { - ConnectionString = "ProjectId=test;CollectionName=test" - }; - var publisher = new MockTelemetryPublisherFirestore(opts); - var mockHttpClient = new MockMinimalHttpClient("http://testing.com"); - mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK)); - - publisher.SetHttpClient(mockHttpClient); - publisher.SetMockData(new Dictionary {["testData"] = 1}); - var cts = new CancellationTokenSource(); - cts.Cancel(); - - // Run - await publisher.PublishAsync(cts.Token); - - // Assert - Assert.Empty(mockHttpClient.SendAsyncArgCalls); - } - - [Fact] - private async Task Test_PublishAsync_Authorization_Refresh() - { - // Setup - var opts = new TelemetryPublisherOptions() - { - ConnectionString = - "ProjectId=test;CollectionName=test;WebApiKey=TAPIKEY;WebApiEmail=t@emai.com;WebApiPassword=tpass" - }; - var publisher = new MockTelemetryPublisherFirestore(opts); - var mockHttpClient = new MockMinimalHttpClient("http://testing.com"); - - mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK) - {Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"0\"}")}); - mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK)); - mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK) - {Content = new StringContent("{\"idToken\": \"1\",\"expiresIn\": \"3600\"}")}); - mockHttpClient.SendAsyncResponses.Add(new HttpResponseMessage(HttpStatusCode.OK)); - - publisher.SetHttpClient(mockHttpClient); - publisher.SetMockData(new Dictionary {["testData"] = 1}); - - // Run - await publisher.PublishAsync(CancellationToken.None); - await publisher.PublishAsync(CancellationToken.None); - - // Assert - Assert.Equal(4, mockHttpClient.SendAsyncArgCalls.Count); - - // 1st request auth - Assert.Equal(HttpMethod.Post, mockHttpClient.SendAsyncArgCalls[0].Method); - Assert.Equal("https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=TAPIKEY", - mockHttpClient.SendAsyncArgCalls[0].RequestUri.ToString()); - - // 2st request payload - Assert.Equal(HttpMethod.Post, mockHttpClient.SendAsyncArgCalls[1].Method); - - // 3rd request auth - Assert.Equal(HttpMethod.Post, mockHttpClient.SendAsyncArgCalls[2].Method); - Assert.Equal("https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=TAPIKEY", - mockHttpClient.SendAsyncArgCalls[2].RequestUri.ToString()); - - // 4th request payload - Assert.Equal(HttpMethod.Post, mockHttpClient.SendAsyncArgCalls[1].Method); - } - } -} \ No newline at end of file