diff --git a/Weather/SerialReader/Controllers/HealthController.cs b/Weather/SerialReader/Controllers/HealthController.cs index 5a4e85c..8101ead 100644 --- a/Weather/SerialReader/Controllers/HealthController.cs +++ b/Weather/SerialReader/Controllers/HealthController.cs @@ -1,27 +1,26 @@ using Microsoft.AspNetCore.Mvc; using System; -namespace ChrisKaczor.HomeMonitor.Weather.SerialReader.Controllers +namespace ChrisKaczor.HomeMonitor.Weather.SerialReader.Controllers; + +[Route("[controller]")] +[ApiController] +public class HealthController : ControllerBase { - [Route("[controller]")] - [ApiController] - public class HealthController : ControllerBase + private readonly TimeSpan _checkTimeSpan = TimeSpan.FromSeconds(5); + + [HttpGet("ready")] + public IActionResult Ready() { - private readonly TimeSpan _checkTimeSpan = TimeSpan.FromSeconds(5); - - [HttpGet("ready")] - public IActionResult Ready() - { - return SerialReader.BoardStarted ? Ok() : Conflict(); - } - - [HttpGet("health")] - public IActionResult Health() - { - var lastReading = SerialReader.LastReading; - var timeSinceLastReading = DateTimeOffset.UtcNow - lastReading; - - return SerialReader.BoardStarted && timeSinceLastReading <= _checkTimeSpan ? Ok(lastReading) : BadRequest(lastReading); - } + return SerialReader.BoardStarted ? Ok() : Conflict(); } -} + + [HttpGet("health")] + public IActionResult Health() + { + var lastReading = SerialReader.LastReading; + var timeSinceLastReading = DateTimeOffset.UtcNow - lastReading; + + return SerialReader.BoardStarted && timeSinceLastReading <= _checkTimeSpan ? Ok(lastReading) : BadRequest(lastReading); + } +} \ No newline at end of file diff --git a/Weather/SerialReader/Dockerfile b/Weather/SerialReader/Dockerfile index 1aa8d74..99638d2 100644 --- a/Weather/SerialReader/Dockerfile +++ b/Weather/SerialReader/Dockerfile @@ -1,8 +1,8 @@ -FROM mcr.microsoft.com/dotnet/aspnet:5.0-focal-arm32v7 AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-arm32v7 AS base WORKDIR /app EXPOSE 80 -FROM mcr.microsoft.com/dotnet/sdk:5.0-focal AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy AS build WORKDIR /src COPY ["./SerialReader.csproj", "./"] RUN dotnet restore -r linux-arm "SerialReader.csproj" diff --git a/Weather/SerialReader/Program.cs b/Weather/SerialReader/Program.cs index f9e9641..a2ee2e0 100644 --- a/Weather/SerialReader/Program.cs +++ b/Weather/SerialReader/Program.cs @@ -1,20 +1,116 @@ -using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using System; +using System.IO.Compression; +using System.Reflection; +using System.Text.Json.Serialization; -namespace ChrisKaczor.HomeMonitor.Weather.SerialReader +namespace ChrisKaczor.HomeMonitor.Weather.SerialReader; + +public static class Program { - public static class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateHostBuilder(args).Build().Run(); - } + var builder = WebApplication.CreateBuilder(args); - private static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => + builder.Configuration.AddEnvironmentVariables(); + + builder.Services.AddControllers(); + + // --- + + var openTelemetry = builder.Services.AddOpenTelemetry(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var serviceName = Assembly.GetExecutingAssembly().GetName().Name; + + openTelemetry.ConfigureResource(resource => resource.AddService(serviceName!)); + + openTelemetry.WithMetrics(meterProviderBuilder => meterProviderBuilder + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddProcessInstrumentation() + .AddMeter("Microsoft.AspNetCore.Hosting") + .AddMeter("Microsoft.AspNetCore.Server.Kestrel")); + + openTelemetry.WithTracing(tracerProviderBuilder => + { + tracerProviderBuilder.AddAspNetCoreInstrumentation(instrumentationOptions => instrumentationOptions.RecordException = true); + + tracerProviderBuilder.AddHttpClientInstrumentation(instrumentationOptions => instrumentationOptions.RecordException = true); + + tracerProviderBuilder.AddSqlClientInstrumentation(o => + { + o.RecordException = true; + o.SetDbStatementForText = true; + }); + + tracerProviderBuilder.AddSource(nameof(SerialReader)); + + tracerProviderBuilder.SetErrorStatusOnException(); + + tracerProviderBuilder.AddOtlpExporter(exporterOptions => + { + exporterOptions.Endpoint = new Uri(builder.Configuration["Telemetry:Endpoint"]!); + exporterOptions.Protocol = OtlpExportProtocol.Grpc; + }); + }); + + builder.Services.AddLogging(loggingBuilder => + { + loggingBuilder.SetMinimumLevel(LogLevel.Information); + loggingBuilder.AddOpenTelemetry(options => { - webBuilder.UseStartup(); - }); + options.AddOtlpExporter(exporterOptions => + { + exporterOptions.Endpoint = new Uri(builder.Configuration["Telemetry:Endpoint"]!); + exporterOptions.Protocol = OtlpExportProtocol.Grpc; + }); + } + ); + }); + + builder.Services.AddHostedService(); + + builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); + + builder.Services.AddResponseCompression(options => + { + options.Providers.Add(); + options.EnableForHttps = true; + }); + + builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", corsPolicyBuilder => corsPolicyBuilder.AllowAnyMethod().AllowAnyHeader().AllowCredentials().WithOrigins("http://localhost:4200"))); + + builder.Services.AddMvc().AddJsonOptions(configure => { configure.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); + + // --- + + var app = builder.Build(); + + if (builder.Environment.IsDevelopment()) + app.UseDeveloperExceptionPage(); + + app.UseCors("CorsPolicy"); + + app.UseResponseCompression(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); } -} +} \ No newline at end of file diff --git a/Weather/SerialReader/SerialReader.cs b/Weather/SerialReader/SerialReader.cs index 662b81e..5e91975 100644 --- a/Weather/SerialReader/SerialReader.cs +++ b/Weather/SerialReader/SerialReader.cs @@ -8,163 +8,158 @@ using System.IO.Ports; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; -namespace ChrisKaczor.HomeMonitor.Weather.SerialReader +namespace ChrisKaczor.HomeMonitor.Weather.SerialReader; + +public class SerialReader(IConfiguration configuration, ILogger logger) : IHostedService { - public class SerialReader : IHostedService + public static bool BoardStarted { get; private set; } + public static DateTimeOffset LastReading { get; private set; } + + private CancellationToken _cancellationToken; + + public Task StartAsync(CancellationToken cancellationToken) { - private readonly IConfiguration _configuration; + _cancellationToken = cancellationToken; - public static bool BoardStarted { get; private set; } - public static DateTimeOffset LastReading { get; private set; } + Task.Run(Execute, cancellationToken); - private CancellationToken _cancellationToken; + return Task.CompletedTask; + } - public SerialReader(IConfiguration configuration) + private void Execute() + { + var baudRate = configuration.GetValue("Weather:Port:BaudRate"); + + while (!_cancellationToken.IsCancellationRequested) { - _configuration = configuration; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _cancellationToken = cancellationToken; - - Task.Run(Execute, cancellationToken); - - return Task.CompletedTask; - } - - private void Execute() - { - var baudRate = int.Parse(_configuration["Weather:Port:BaudRate"]); - - while (!_cancellationToken.IsCancellationRequested) + try { - try - { - WriteLog("Starting main loop"); - - Thread.Sleep(1000); - - var port = GetPort(); - - if (port == null) - continue; - - using var serialPort = new SerialPort(port, baudRate) { NewLine = "\r\n" }; - - WriteLog("Opening serial port"); - - serialPort.Open(); - - BoardStarted = false; - - var factory = new ConnectionFactory - { - HostName = _configuration["Weather:Queue:Host"], - UserName = _configuration["Weather:Queue:User"], - Password = _configuration["Weather:Queue:Password"] - }; - - WriteLog("Connecting to queue server"); - - using var connection = factory.CreateConnection(); - using var model = connection.CreateModel(); - - WriteLog("Declaring queue"); - - model.QueueDeclare(_configuration["Weather:Queue:Name"], true, false, false, null); - - WriteLog("Starting serial read loop"); - - ReadSerial(serialPort, model); - } - catch (Exception exception) - { - WriteLog($"Exception: {exception}"); - } - } - } - - private void ReadSerial(SerialPort serialPort, IModel model) - { - while (!_cancellationToken.IsCancellationRequested) - { - try - { - var message = serialPort.ReadLine(); - - if (!BoardStarted) - { - BoardStarted = message.Contains("Board starting"); - - if (BoardStarted) - WriteLog("Board started"); - } - - if (!BoardStarted) - { - WriteLog($"Message received but board not started: {message}"); - continue; - } - - LastReading = DateTimeOffset.UtcNow; - - WriteLog($"Message received: {message}"); - - var weatherMessage = WeatherMessage.Parse(message); - - var messageString = JsonConvert.SerializeObject(weatherMessage); - - var body = Encoding.UTF8.GetBytes(messageString); - - var properties = model.CreateBasicProperties(); - - properties.Persistent = true; - - model.BasicPublish(string.Empty, _configuration["Weather:Queue:Name"], properties, body); - } - catch (TimeoutException) - { - WriteLog("Serial port read timeout"); - } - } - } - - private string GetPort() - { - var portPrefix = _configuration["Weather:Port:Prefix"]; - - while (!_cancellationToken.IsCancellationRequested) - { - WriteLog($"Checking for port starting with: {portPrefix}"); - - var ports = SerialPort.GetPortNames(); - - var port = Array.Find(ports, p => p.StartsWith(portPrefix)); - - if (port != null) - { - WriteLog($"Port found: {port}"); - return port; - } - - WriteLog("Port not found - waiting"); + WriteLog("Starting main loop"); Thread.Sleep(1000); + + var port = GetPort(); + + if (port == null) + continue; + + using var serialPort = new SerialPort(port, baudRate); + + serialPort.NewLine = "\r\n"; + + WriteLog("Opening serial port"); + + serialPort.Open(); + + BoardStarted = false; + + var factory = new ConnectionFactory + { + HostName = configuration["Weather:Queue:Host"], + UserName = configuration["Weather:Queue:User"], + Password = configuration["Weather:Queue:Password"] + }; + + WriteLog("Connecting to queue server"); + + using var connection = factory.CreateConnection(); + using var model = connection.CreateModel(); + + WriteLog("Declaring queue"); + + model.QueueDeclare(configuration["Weather:Queue:Name"], true, false, false, null); + + WriteLog("Starting serial read loop"); + + ReadSerial(serialPort, model); + } + catch (Exception exception) + { + WriteLog($"Exception: {exception}"); } - - return null; - } - - private static void WriteLog(string message) - { - Console.WriteLine(message); - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; } } -} + + private void ReadSerial(SerialPort serialPort, IModel model) + { + while (!_cancellationToken.IsCancellationRequested) + { + try + { + var message = serialPort.ReadLine(); + + if (!BoardStarted) + { + BoardStarted = message.Contains("Board starting"); + + if (BoardStarted) + WriteLog("Board started"); + } + + if (!BoardStarted) + { + WriteLog($"Message received but board not started: {message}"); + continue; + } + + LastReading = DateTimeOffset.UtcNow; + + WriteLog($"Message received: {message}"); + + var weatherMessage = WeatherMessage.Parse(message); + + var messageString = JsonConvert.SerializeObject(weatherMessage); + + var body = Encoding.UTF8.GetBytes(messageString); + + var properties = model.CreateBasicProperties(); + + properties.Persistent = true; + + model.BasicPublish(string.Empty, configuration["Weather:Queue:Name"], properties, body); + } + catch (TimeoutException) + { + WriteLog("Serial port read timeout"); + } + } + } + + private string GetPort() + { + var portPrefix = configuration["Weather:Port:Prefix"]!; + + while (!_cancellationToken.IsCancellationRequested) + { + WriteLog($"Checking for port starting with: {portPrefix}"); + + var ports = SerialPort.GetPortNames(); + + var port = Array.Find(ports, p => p.StartsWith(portPrefix)); + + if (port != null) + { + WriteLog($"Port found: {port}"); + return port; + } + + WriteLog("Port not found - waiting"); + + Thread.Sleep(1000); + } + + return null; + } + + private void WriteLog(string message) + { + logger.LogInformation(message); + } + + public Task StopAsync(CancellationToken cancellationToken) + { + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/Weather/SerialReader/SerialReader.csproj b/Weather/SerialReader/SerialReader.csproj index bea55d8..e466d1d 100644 --- a/Weather/SerialReader/SerialReader.csproj +++ b/Weather/SerialReader/SerialReader.csproj @@ -2,8 +2,7 @@ Exe - net5.0 - ../../ChrisKaczor.ruleset + net8.0 false @@ -13,13 +12,16 @@ - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/Weather/SerialReader/Startup.cs b/Weather/SerialReader/Startup.cs deleted file mode 100644 index b37733e..0000000 --- a/Weather/SerialReader/Startup.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; - -namespace ChrisKaczor.HomeMonitor.Weather.SerialReader -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddControllers(); - - services.AddHostedService(); - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseRouting(); - - app.UseAuthorization(); - - app.UseEndpoints(endpoints => - { - endpoints.MapControllers(); - }); - } - } -} diff --git a/Weather/SerialReader/appsettings.json b/Weather/SerialReader/appsettings.json index 3c10255..cbe2868 100644 --- a/Weather/SerialReader/appsettings.json +++ b/Weather/SerialReader/appsettings.json @@ -7,5 +7,8 @@ "Queue": { "Name": "weather" } + }, + "Telemetry": { + "Endpoint": "http://signoz-otel-collector.platform:4317/" } } \ No newline at end of file diff --git a/Weather/SerialReader/deploy/manifest.yaml b/Weather/SerialReader/deploy/manifest.yaml index 934b58b..d00095b 100644 --- a/Weather/SerialReader/deploy/manifest.yaml +++ b/Weather/SerialReader/deploy/manifest.yaml @@ -97,7 +97,7 @@ spec: livenessProbe: httpGet: path: health/health - port: 80 + port: 8080 env: - name: Weather__Queue__Host value: weather-queue diff --git a/Weather/Service/Controllers/ReadingsController.cs b/Weather/Service/Controllers/ReadingsController.cs index ef50646..46e2062 100644 --- a/Weather/Service/Controllers/ReadingsController.cs +++ b/Weather/Service/Controllers/ReadingsController.cs @@ -7,78 +7,70 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Controllers +namespace ChrisKaczor.HomeMonitor.Weather.Service.Controllers; + +[Route("[controller]")] +[ApiController] +public class ReadingsController(Database database) : ControllerBase { - [Route("[controller]")] - [ApiController] - public class ReadingsController : ControllerBase + [HttpGet("recent")] + public async Task> GetRecent([FromQuery] string tz) { - private readonly Database _database; - - public ReadingsController(Database database) - { - _database = database; - } - - [HttpGet("recent")] - public async Task> GetRecent([FromQuery] string tz) - { - var recentReading = await _database.GetRecentReading(); - - if (string.IsNullOrWhiteSpace(tz)) - return recentReading; - - try - { - var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(tz); - - recentReading.Timestamp = recentReading.Timestamp.ToOffset(timeZoneInfo.GetUtcOffset(recentReading.Timestamp)); - recentReading.GpsTimestamp = recentReading.GpsTimestamp.ToOffset(timeZoneInfo.GetUtcOffset(recentReading.GpsTimestamp)); - } - catch (Exception e) - { - return BadRequest(e.Message); - } + var recentReading = await database.GetRecentReading(); + if (string.IsNullOrWhiteSpace(tz)) return recentReading; - } - [HttpGet("history")] - public async Task>> GetHistory(DateTimeOffset start, DateTimeOffset end) + try { - return (await _database.GetReadingHistory(start, end)).ToList(); - } + var timeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById(tz); - [HttpGet("aggregate")] - public async Task> GetHistoryAggregate(DateTimeOffset start, DateTimeOffset end) + recentReading.Timestamp = recentReading.Timestamp.ToOffset(timeZoneInfo.GetUtcOffset(recentReading.Timestamp)); + recentReading.GpsTimestamp = recentReading.GpsTimestamp.ToOffset(timeZoneInfo.GetUtcOffset(recentReading.GpsTimestamp)); + } + catch (Exception e) { - var readings = (await _database.GetReadingHistory(start, end)).ToList(); - - return readings.Any() ? new WeatherAggregate(readings) : null; + return BadRequest(e.Message); } - [HttpGet("value-history")] - public async Task>> GetValueHistory(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end) - { - return (await _database.GetReadingValueHistory(weatherValueType, start, end)).ToList(); - } + return recentReading; + } - [HttpGet("value-history-grouped")] - public async Task>> GetValueHistoryGrouped(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end, int bucketMinutes = 2) - { - return (await _database.GetReadingValueHistoryGrouped(weatherValueType, start, end, bucketMinutes)).ToList(); - } + [HttpGet("history")] + public async Task>> GetHistory(DateTimeOffset start, DateTimeOffset end) + { + return (await database.GetReadingHistory(start, end)).ToList(); + } - [HttpGet("history-grouped")] - public async Task>> GetHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes = 2) - { - return (await _database.GetReadingHistoryGrouped(start, end, bucketMinutes)).ToList(); - } + [HttpGet("aggregate")] + public async Task> GetHistoryAggregate(DateTimeOffset start, DateTimeOffset end) + { + var readings = (await database.GetReadingHistory(start, end)).ToList(); - [HttpGet("wind-history-grouped")] - public async Task>> GetWindHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes = 60) - { - return (await _database.GetWindHistoryGrouped(start, end, bucketMinutes)).ToList(); - } + return readings.Any() ? new WeatherAggregate(readings) : null; + } + + [HttpGet("value-history")] + public async Task>> GetValueHistory(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end) + { + return (await database.GetReadingValueHistory(weatherValueType, start, end)).ToList(); + } + + [HttpGet("value-history-grouped")] + public async Task>> GetValueHistoryGrouped(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end, int bucketMinutes = 2) + { + return (await database.GetReadingValueHistoryGrouped(weatherValueType, start, end, bucketMinutes)).ToList(); + } + + [HttpGet("history-grouped")] + public async Task>> GetHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes = 2) + { + return (await database.GetReadingHistoryGrouped(start, end, bucketMinutes)).ToList(); + } + + [HttpGet("wind-history-grouped")] + public async Task>> GetWindHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes = 60) + { + return (await database.GetWindHistoryGrouped(start, end, bucketMinutes)).ToList(); } } \ No newline at end of file diff --git a/Weather/Service/Data/Database.cs b/Weather/Service/Data/Database.cs index eaa0a93..7ca226d 100644 --- a/Weather/Service/Data/Database.cs +++ b/Weather/Service/Data/Database.cs @@ -7,146 +7,138 @@ using System.Collections.Generic; using System.Data.SqlClient; using System.Threading.Tasks; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Data +namespace ChrisKaczor.HomeMonitor.Weather.Service.Data; + +public class Database(IConfiguration configuration) { - public class Database + public void EnsureDatabase() { - private readonly IConfiguration _configuration; - - public Database(IConfiguration configuration) + var connectionStringBuilder = new SqlConnectionStringBuilder { - _configuration = configuration; - } + DataSource = configuration["Weather:Database:Host"], + UserID = configuration["Weather:Database:User"], + Password = configuration["Weather:Database:Password"], + InitialCatalog = "master" + }; - public void EnsureDatabase() + using var connection = new SqlConnection(connectionStringBuilder.ConnectionString); + + var command = new SqlCommand { Connection = connection }; + + connection.Open(); + + // Check to see if the database exists + command.CommandText = $"SELECT CAST(1 as bit) from sys.databases WHERE name='{configuration["Weather:Database:Name"]}'"; + var databaseExists = (bool?)command.ExecuteScalar(); + + // Create database if needed + if (!(databaseExists ?? false)) { - var connectionStringBuilder = new SqlConnectionStringBuilder - { - DataSource = _configuration["Weather:Database:Host"], - UserID = _configuration["Weather:Database:User"], - Password = _configuration["Weather:Database:Password"], - InitialCatalog = "master" - }; - - using var connection = new SqlConnection(connectionStringBuilder.ConnectionString); - - var command = new SqlCommand { Connection = connection }; - - connection.Open(); - - // Check to see if the database exists - command.CommandText = $"SELECT CAST(1 as bit) from sys.databases WHERE name='{_configuration["Weather:Database:Name"]}'"; - var databaseExists = (bool?)command.ExecuteScalar(); - - // Create database if needed - if (!(databaseExists ?? false)) - { - command.CommandText = $"CREATE DATABASE {_configuration["Weather:Database:Name"]}"; - command.ExecuteNonQuery(); - } - - // Switch to the database now that we're sure it exists - connection.ChangeDatabase(_configuration["Weather:Database:Name"]); - - var schema = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.Schema.sql"); - - // Make sure the database is up to date - command.CommandText = schema; + command.CommandText = $"CREATE DATABASE {configuration["Weather:Database:Name"]}"; command.ExecuteNonQuery(); } - private SqlConnection CreateConnection() + // Switch to the database now that we're sure it exists + connection.ChangeDatabase(configuration["Weather:Database:Name"]!); + + var schema = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.Schema.sql"); + + // Make sure the database is up-to-date + command.CommandText = schema; + command.ExecuteNonQuery(); + } + + private SqlConnection CreateConnection() + { + var connectionStringBuilder = new SqlConnectionStringBuilder { - var connectionStringBuilder = new SqlConnectionStringBuilder - { - DataSource = _configuration["Weather:Database:Host"], - UserID = _configuration["Weather:Database:User"], - Password = _configuration["Weather:Database:Password"], - InitialCatalog = _configuration["Weather:Database:Name"] - }; + DataSource = configuration["Weather:Database:Host"], + UserID = configuration["Weather:Database:User"], + Password = configuration["Weather:Database:Password"], + InitialCatalog = configuration["Weather:Database:Name"] + }; - var connection = new SqlConnection(connectionStringBuilder.ConnectionString); - connection.Open(); + var connection = new SqlConnection(connectionStringBuilder.ConnectionString); + connection.Open(); - return connection; - } + return connection; + } - public void StoreWeatherData(WeatherMessage weatherMessage) - { - using var connection = CreateConnection(); + public void StoreWeatherData(WeatherMessage weatherMessage) + { + using var connection = CreateConnection(); - var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.CreateReading.sql"); + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.CreateReading.sql"); - connection.Query(query, weatherMessage); - } + connection.Query(query, weatherMessage); + } - public async Task GetRecentReading() - { - await using var connection = CreateConnection(); + public async Task GetRecentReading() + { + await using var connection = CreateConnection(); - var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetRecentReading.sql"); + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetRecentReading.sql"); - return await connection.QueryFirstOrDefaultAsync(query).ConfigureAwait(false); - } + return await connection.QueryFirstOrDefaultAsync(query).ConfigureAwait(false); + } - public async Task> GetReadingHistory(DateTimeOffset start, DateTimeOffset end) - { - await using var connection = CreateConnection(); + public async Task> GetReadingHistory(DateTimeOffset start, DateTimeOffset end) + { + await using var connection = CreateConnection(); - var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingHistory.sql"); + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingHistory.sql"); - return await connection.QueryAsync(query, new { Start = start, End = end }); - } + return await connection.QueryAsync(query, new { Start = start, End = end }); + } - public async Task> GetReadingValueHistory(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end) - { - await using var connection = CreateConnection(); + public async Task> GetReadingValueHistory(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end) + { + await using var connection = CreateConnection(); - var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingValueHistory.sql"); + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingValueHistory.sql"); - query = query.Replace("@Value", weatherValueType.ToString()); + query = query.Replace("@Value", weatherValueType.ToString()); - return await connection.QueryAsync(query, new { Start = start, End = end }); - } + return await connection.QueryAsync(query, new { Start = start, End = end }); + } - public async Task GetReadingValueSum(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end) - { - await using var connection = CreateConnection(); + public async Task GetReadingValueSum(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end) + { + await using var connection = CreateConnection(); - var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingValueSum.sql"); + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingValueSum.sql"); - query = query.Replace("@Value", weatherValueType.ToString()); + query = query.Replace("@Value", weatherValueType.ToString()); - return await connection.ExecuteScalarAsync(query, new { Start = start, End = end }); - } + return await connection.ExecuteScalarAsync(query, new { Start = start, End = end }); + } - public async Task> GetReadingValueHistoryGrouped(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end, int bucketMinutes) - { - await using var connection = CreateConnection(); + public async Task> GetReadingValueHistoryGrouped(WeatherValueType weatherValueType, DateTimeOffset start, DateTimeOffset end, int bucketMinutes) + { + await using var connection = CreateConnection(); - var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingValueHistoryGrouped.sql"); + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingValueHistoryGrouped.sql"); - query = query.Replace("@Value", weatherValueType.ToString()); + query = query.Replace("@Value", weatherValueType.ToString()); - return await connection.QueryAsync(query, new { Start = start, End = end, BucketMinutes = bucketMinutes }); - } + return await connection.QueryAsync(query, new { Start = start, End = end, BucketMinutes = bucketMinutes }); + } - public async Task> GetReadingHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes) - { - await using var connection = CreateConnection(); + public async Task> GetReadingHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes) + { + await using var connection = CreateConnection(); - var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingHistoryGrouped.sql"); + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetReadingHistoryGrouped.sql"); - return await connection.QueryAsync(query, new { Start = start, End = end, BucketMinutes = bucketMinutes }); - } + return await connection.QueryAsync(query, new { Start = start, End = end, BucketMinutes = bucketMinutes }); + } - public async Task> GetWindHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes) - { - await using var connection = CreateConnection(); + public async Task> GetWindHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes) + { + await using var connection = CreateConnection(); - var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetWindHistoryGrouped.sql"); + var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Weather.Service.Data.Resources.GetWindHistoryGrouped.sql"); - return await connection.QueryAsync(query, new { Start = start, End = end, BucketMinutes = bucketMinutes }); - } + return await connection.QueryAsync(query, new { Start = start, End = end, BucketMinutes = bucketMinutes }); } } \ No newline at end of file diff --git a/Weather/Service/Dockerfile b/Weather/Service/Dockerfile index e23636d..b8d559b 100644 --- a/Weather/Service/Dockerfile +++ b/Weather/Service/Dockerfile @@ -1,8 +1,8 @@ -FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine AS base +FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base WORKDIR /app -EXPOSE 80 +EXPOSE 8080 -FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build WORKDIR /src COPY ["./Service.csproj", "./"] RUN dotnet restore "Service.csproj" @@ -11,7 +11,8 @@ WORKDIR "/src" RUN dotnet publish "Service.csproj" -c Release -o /app FROM base AS final -RUN apk add --no-cache tzdata WORKDIR /app COPY --from=build /app . +RUN apk add --no-cache icu-libs tzdata +ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false ENTRYPOINT ["dotnet", "ChrisKaczor.HomeMonitor.Weather.Service.dll"] \ No newline at end of file diff --git a/Weather/Service/Extensions.cs b/Weather/Service/Extensions.cs index 693a2d1..ff6f7d1 100644 --- a/Weather/Service/Extensions.cs +++ b/Weather/Service/Extensions.cs @@ -1,18 +1,17 @@ -using System; -using DecimalMath; +using DecimalMath; +using System; -namespace ChrisKaczor.HomeMonitor.Weather.Service +namespace ChrisKaczor.HomeMonitor.Weather.Service; + +public static class Extensions { - public static class Extensions + public static bool IsBetween(this T item, T start, T end) where T : IComparable { - public static bool IsBetween(this T item, T start, T end) where T : IComparable - { - return item.CompareTo(start) >= 0 && item.CompareTo(end) <= 0; - } - - public static decimal Truncate(this decimal value, int decimalPlaces) - { - return decimal.Truncate(value * DecimalEx.Pow(10, decimalPlaces)) / DecimalEx.Pow(10, decimalPlaces); - } + return item.CompareTo(start) >= 0 && item.CompareTo(end) <= 0; } -} + + public static decimal Truncate(this decimal value, int decimalPlaces) + { + return decimal.Truncate(value * DecimalEx.Pow(10, decimalPlaces)) / DecimalEx.Pow(10, decimalPlaces); + } +} \ No newline at end of file diff --git a/Weather/Service/MessageHandler.cs b/Weather/Service/MessageHandler.cs index bc630f1..5b22c10 100644 --- a/Weather/Service/MessageHandler.cs +++ b/Weather/Service/MessageHandler.cs @@ -13,119 +13,109 @@ using System.Text; using System.Threading; using System.Threading.Tasks; -namespace ChrisKaczor.HomeMonitor.Weather.Service +namespace ChrisKaczor.HomeMonitor.Weather.Service; + +[UsedImplicitly] +public class MessageHandler(IConfiguration configuration, Database database) : IHostedService { - [UsedImplicitly] - public class MessageHandler : IHostedService + private IConnection _queueConnection; + private IModel _queueModel; + + private HubConnection _hubConnection; + + public Task StartAsync(CancellationToken cancellationToken) { - private readonly IConfiguration _configuration; - private readonly Database _database; + var host = configuration["Weather:Queue:Host"]; - private IConnection _queueConnection; - private IModel _queueModel; + if (string.IsNullOrEmpty(host)) + return Task.CompletedTask; - private HubConnection _hubConnection; + WriteLog("MessageHandler: Start"); - public MessageHandler(IConfiguration configuration, Database database) + var factory = new ConnectionFactory { - _configuration = configuration; - _database = database; - } + HostName = host, + UserName = configuration["Weather:Queue:User"], + Password = configuration["Weather:Queue:Password"] + }; - public Task StartAsync(CancellationToken cancellationToken) + _queueConnection = factory.CreateConnection(); + _queueModel = _queueConnection.CreateModel(); + + _queueModel.QueueDeclare(configuration["Weather:Queue:Name"], true, false, false, null); + + var consumer = new EventingBasicConsumer(_queueModel); + consumer.Received += DeviceEventHandler; + + _queueModel.BasicConsume(configuration["Weather:Queue:Name"], true, consumer); + + if (!string.IsNullOrEmpty(configuration["Hub:Weather"])) + _hubConnection = new HubConnectionBuilder().WithUrl(configuration["Hub:Weather"]).Build(); + + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + WriteLog("MessageHandler: Stop"); + + _hubConnection?.StopAsync(cancellationToken).Wait(cancellationToken); + + _queueModel?.Close(); + _queueConnection?.Close(); + + return Task.CompletedTask; + } + + private void DeviceEventHandler(object model, BasicDeliverEventArgs eventArgs) + { + try { - var host = _configuration["Weather:Queue:Host"]; + var body = eventArgs.Body; + var message = Encoding.UTF8.GetString(body.ToArray()); - if (string.IsNullOrEmpty(host)) - return Task.CompletedTask; + WriteLog($"Message received: {message}"); - WriteLog("MessageHandler: Start"); + var weatherMessage = JsonConvert.DeserializeObject(message); - var factory = new ConnectionFactory + if (weatherMessage.Type == MessageType.Text) { - HostName = host, - UserName = _configuration["Weather:Queue:User"], - Password = _configuration["Weather:Queue:Password"] - }; + WriteLog(weatherMessage.Message); - _queueConnection = factory.CreateConnection(); - _queueModel = _queueConnection.CreateModel(); + return; + } - _queueModel.QueueDeclare(_configuration["Weather:Queue:Name"], true, false, false, null); + database.StoreWeatherData(weatherMessage); - var consumer = new EventingBasicConsumer(_queueModel); - consumer.Received += DeviceEventHandler; + if (_hubConnection == null) + return; - _queueModel.BasicConsume(_configuration["Weather:Queue:Name"], true, consumer); + var weatherUpdate = new WeatherUpdate(weatherMessage, database); - if (!string.IsNullOrEmpty(_configuration["Hub:Weather"])) - _hubConnection = new HubConnectionBuilder().WithUrl(_configuration["Hub:Weather"]).Build(); - - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - WriteLog("MessageHandler: Stop"); - - _hubConnection?.StopAsync(cancellationToken).Wait(cancellationToken); - - _queueModel?.Close(); - _queueConnection?.Close(); - - return Task.CompletedTask; - } - - private void DeviceEventHandler(object model, BasicDeliverEventArgs eventArgs) - { try { - var body = eventArgs.Body; - var message = Encoding.UTF8.GetString(body.ToArray()); + if (_hubConnection.State == HubConnectionState.Disconnected) + _hubConnection.StartAsync().Wait(); - WriteLog($"Message received: {message}"); + var json = JsonConvert.SerializeObject(weatherUpdate); - var weatherMessage = JsonConvert.DeserializeObject(message); - - if (weatherMessage.Type == MessageType.Text) - { - WriteLog(weatherMessage.Message); - - return; - } - - _database.StoreWeatherData(weatherMessage); - - if (_hubConnection == null) - return; - - var weatherUpdate = new WeatherUpdate(weatherMessage, _database); - - try - { - if (_hubConnection.State == HubConnectionState.Disconnected) - _hubConnection.StartAsync().Wait(); - - var json = JsonConvert.SerializeObject(weatherUpdate); - - _hubConnection.InvokeAsync("SendLatestReading", json).Wait(); - } - catch (Exception exception) - { - WriteLog($"Hub exception: {exception}"); - } + _hubConnection.InvokeAsync("SendLatestReading", json).Wait(); } catch (Exception exception) { - WriteLog($"Exception: {exception}"); - - throw; + WriteLog($"Hub exception: {exception}"); } } - - private static void WriteLog(string message) + catch (Exception exception) { - Console.WriteLine(message); + WriteLog($"Exception: {exception}"); + + throw; } } -} + + private static void WriteLog(string message) + { + Console.WriteLine(message); + } +} \ No newline at end of file diff --git a/Weather/Service/Models/ReadingAggregate.cs b/Weather/Service/Models/ReadingAggregate.cs index 9699dc4..32c4fbb 100644 --- a/Weather/Service/Models/ReadingAggregate.cs +++ b/Weather/Service/Models/ReadingAggregate.cs @@ -1,25 +1,17 @@ +using ChrisKaczor.HomeMonitor.Weather.Models; +using JetBrains.Annotations; using System; using System.Collections.Generic; using System.Linq; -using ChrisKaczor.HomeMonitor.Weather.Models; -using JetBrains.Annotations; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Models +namespace ChrisKaczor.HomeMonitor.Weather.Service.Models; + +[PublicAPI] +public class ReadingAggregate(IEnumerable readings, Func selector, int decimalPlaces) { - [PublicAPI] - public class ReadingAggregate - { - public decimal Min { get; set; } + public decimal Min { get; set; } = readings.Min(selector); - public decimal Max { get; set; } + public decimal Max { get; set; } = readings.Max(selector); - public decimal Average { get; set; } - - public ReadingAggregate(IEnumerable readings, Func selector, int decimalPlaces) - { - Min = readings.Min(selector); - Max = readings.Max(selector); - Average = readings.Average(selector).Truncate(decimalPlaces); - } - } + public decimal Average { get; set; } = readings.Average(selector).Truncate(decimalPlaces); } \ No newline at end of file diff --git a/Weather/Service/Models/WeatherAggregate.cs b/Weather/Service/Models/WeatherAggregate.cs index 4298862..0e4c9d1 100644 --- a/Weather/Service/Models/WeatherAggregate.cs +++ b/Weather/Service/Models/WeatherAggregate.cs @@ -1,49 +1,48 @@ +using ChrisKaczor.HomeMonitor.Weather.Models; +using JetBrains.Annotations; using System; using System.Collections.Generic; using System.Linq; -using ChrisKaczor.HomeMonitor.Weather.Models; -using JetBrains.Annotations; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Models +namespace ChrisKaczor.HomeMonitor.Weather.Service.Models; + +[PublicAPI] +public class WeatherAggregate { - [PublicAPI] - public class WeatherAggregate + public ReadingAggregate Humidity { get; set; } + + public ReadingAggregate Temperature { get; set; } + + public ReadingAggregate Pressure { get; set; } + + public ReadingAggregate Light { get; set; } + + public ReadingAggregate WindSpeed { get; set; } + + public WindDirection WindDirectionAverage { get; set; } + + public decimal RainTotal { get; set; } + + private static readonly List WindDirectionValues = ((WindDirection[])Enum.GetValues(typeof(WindDirection))).Select(e => (int)e).ToList(); + + public WeatherAggregate(List readings) { - public ReadingAggregate Humidity { get; set; } + if (!readings.Any()) + return; - public ReadingAggregate Temperature { get; set; } + Humidity = new ReadingAggregate(readings, r => r.Humidity, 1); - public ReadingAggregate Pressure { get; set; } + Temperature = new ReadingAggregate(readings, r => r.HumidityTemperature, 1); - public ReadingAggregate Light { get; set; } + Pressure = new ReadingAggregate(readings, r => r.Pressure, 2); - public ReadingAggregate WindSpeed { get; set; } + Light = new ReadingAggregate(readings, r => r.LightLevel, 2); - public WindDirection WindDirectionAverage { get; set; } + WindSpeed = new ReadingAggregate(readings, r => r.WindSpeed, 1); - public decimal RainTotal { get; set; } + var windDirectionAverage = readings.Average(r => (decimal)r.WindDirection); + WindDirectionAverage = (WindDirection)WindDirectionValues.Aggregate((x, y) => Math.Abs(x - windDirectionAverage) < Math.Abs(y - windDirectionAverage) ? x : y); - private static readonly List WindDirectionValues = ((WindDirection[])Enum.GetValues(typeof(WindDirection))).Select(e => (int)e).ToList(); - - public WeatherAggregate(List readings) - { - if (!readings.Any()) - return; - - Humidity = new ReadingAggregate(readings, r => r.Humidity, 1); - - Temperature = new ReadingAggregate(readings, r => r.HumidityTemperature, 1); - - Pressure = new ReadingAggregate(readings, r => r.Pressure, 2); - - Light = new ReadingAggregate(readings, r => r.LightLevel, 2); - - WindSpeed = new ReadingAggregate(readings, r => r.WindSpeed, 1); - - var windDirectionAverage = readings.Average(r => (decimal)r.WindDirection); - WindDirectionAverage = (WindDirection)WindDirectionValues.Aggregate((x, y) => Math.Abs(x - windDirectionAverage) < Math.Abs(y - windDirectionAverage) ? x : y); - - RainTotal = readings.Sum(r => r.Rain); - } + RainTotal = readings.Sum(r => r.Rain); } } \ No newline at end of file diff --git a/Weather/Service/Models/WeatherReadingGrouped.cs b/Weather/Service/Models/WeatherReadingGrouped.cs index 8d20b97..6d268f5 100644 --- a/Weather/Service/Models/WeatherReadingGrouped.cs +++ b/Weather/Service/Models/WeatherReadingGrouped.cs @@ -1,21 +1,20 @@ using JetBrains.Annotations; using System; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Models +namespace ChrisKaczor.HomeMonitor.Weather.Service.Models; + +[PublicAPI] +public class WeatherReadingGrouped { - [PublicAPI] - public class WeatherReadingGrouped - { - public DateTimeOffset Bucket { get; set; } + public DateTimeOffset Bucket { get; set; } - public decimal AverageHumidity { get; set; } + public decimal AverageHumidity { get; set; } - public decimal AverageTemperature { get; set; } + public decimal AverageTemperature { get; set; } - public decimal AveragePressure { get; set; } + public decimal AveragePressure { get; set; } - public decimal AverageLightLevel { get; set; } + public decimal AverageLightLevel { get; set; } - public decimal RainTotal { get; set; } - } + public decimal RainTotal { get; set; } } \ No newline at end of file diff --git a/Weather/Service/Models/WeatherUpdate.cs b/Weather/Service/Models/WeatherUpdate.cs index 71f0c12..138bd91 100644 --- a/Weather/Service/Models/WeatherUpdate.cs +++ b/Weather/Service/Models/WeatherUpdate.cs @@ -5,97 +5,96 @@ using JetBrains.Annotations; using MathNet.Numerics; using System.Linq; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Models +namespace ChrisKaczor.HomeMonitor.Weather.Service.Models; + +[PublicAPI] +public class WeatherUpdate : WeatherUpdateBase { - [PublicAPI] - public class WeatherUpdate : WeatherUpdateBase + private readonly Database _database; + + public WeatherUpdate(WeatherMessage weatherMessage, Database database) { - private readonly Database _database; + _database = database; - public WeatherUpdate(WeatherMessage weatherMessage, Database database) + Type = weatherMessage.Type; + Message = weatherMessage.Message; + Timestamp = weatherMessage.Timestamp; + WindDirection = weatherMessage.WindDirection; + WindSpeed = weatherMessage.WindSpeed; + Humidity = weatherMessage.Humidity; + Rain = weatherMessage.Rain; + Pressure = weatherMessage.Pressure; + Temperature = weatherMessage.HumidityTemperature; + LightLevel = weatherMessage.LightLevel; + Latitude = weatherMessage.Latitude; + Longitude = weatherMessage.Longitude; + Altitude = weatherMessage.Altitude; + SatelliteCount = weatherMessage.SatelliteCount; + GpsTimestamp = weatherMessage.GpsTimestamp; + + CalculateHeatIndex(); + CalculateWindChill(); + CalculateDewPoint(); + CalculatePressureTrend(); + CalculateRainLastHour(); + } + + private void CalculateRainLastHour() + { + RainLastHour = _database.GetReadingValueSum(WeatherValueType.Rain, Timestamp.AddHours(-1), Timestamp).Result; + } + + private void CalculateWindChill() + { + var temperatureInF = Temperature; + var windSpeedInMph = WindSpeed; + + if (temperatureInF.IsBetween(-45, 45) && windSpeedInMph.IsBetween(3, 60)) { - _database = database; - - Type = weatherMessage.Type; - Message = weatherMessage.Message; - Timestamp = weatherMessage.Timestamp; - WindDirection = weatherMessage.WindDirection; - WindSpeed = weatherMessage.WindSpeed; - Humidity = weatherMessage.Humidity; - Rain = weatherMessage.Rain; - Pressure = weatherMessage.Pressure; - Temperature = weatherMessage.HumidityTemperature; - LightLevel = weatherMessage.LightLevel; - Latitude = weatherMessage.Latitude; - Longitude = weatherMessage.Longitude; - Altitude = weatherMessage.Altitude; - SatelliteCount = weatherMessage.SatelliteCount; - GpsTimestamp = weatherMessage.GpsTimestamp; - - CalculateHeatIndex(); - CalculateWindChill(); - CalculateDewPoint(); - CalculatePressureTrend(); - CalculateRainLastHour(); - } - - private void CalculateRainLastHour() - { - RainLastHour = _database.GetReadingValueSum(WeatherValueType.Rain, Timestamp.AddHours(-1), Timestamp).Result; - } - - private void CalculateWindChill() - { - var temperatureInF = Temperature; - var windSpeedInMph = WindSpeed; - - if (temperatureInF.IsBetween(-45, 45) && windSpeedInMph.IsBetween(3, 60)) - { - WindChill = 35.74m + 0.6215m * temperatureInF - 35.75m * DecimalEx.Pow(windSpeedInMph, 0.16m) + 0.4275m * temperatureInF * DecimalEx.Pow(windSpeedInMph, 0.16m); - } - } - - private void CalculateDewPoint() - { - var relativeHumidity = Humidity; - var temperatureInF = Temperature; - - var temperatureInC = (temperatureInF - 32.0m) * 5.0m / 9.0m; - - var vaporPressure = relativeHumidity * 0.01m * 6.112m * DecimalEx.Exp(17.62m * temperatureInC / (temperatureInC + 243.12m)); - var numerator = 243.12m * DecimalEx.Log(vaporPressure) - 440.1m; - var denominator = 19.43m - DecimalEx.Log(vaporPressure); - var dewPointInC = numerator / denominator; - - DewPoint = dewPointInC * 9.0m / 5.0m + 32.0m; - } - - private void CalculatePressureTrend() - { - var pressureData = _database - .GetReadingValueHistory(WeatherValueType.Pressure, Timestamp.AddHours(-3), Timestamp).Result.ToList(); - - var xData = pressureData.Select(p => (double)p.Timestamp.ToUnixTimeSeconds()).ToArray(); - var yData = pressureData.Select(p => (double)p.Value / 100.0).ToArray(); - - var lineFunction = Fit.LineFunc(xData, yData); - - PressureDifferenceThreeHour = (decimal)(lineFunction(xData.Last()) - lineFunction(xData[0])); - } - - private void CalculateHeatIndex() - { - var temperature = Temperature; - var humidity = Humidity; - - if (temperature.IsBetween(80, 100) && humidity.IsBetween(40, 100)) - { - HeatIndex = -42.379m + 2.04901523m * temperature + 10.14333127m * humidity - - .22475541m * temperature * humidity - .00683783m * temperature * temperature - - .05481717m * humidity * humidity + .00122874m * temperature * temperature * humidity + - .00085282m * temperature * humidity * humidity - - .00000199m * temperature * temperature * humidity * humidity; - } + WindChill = 35.74m + 0.6215m * temperatureInF - 35.75m * DecimalEx.Pow(windSpeedInMph, 0.16m) + 0.4275m * temperatureInF * DecimalEx.Pow(windSpeedInMph, 0.16m); } } -} + + private void CalculateDewPoint() + { + var relativeHumidity = Humidity; + var temperatureInF = Temperature; + + var temperatureInC = (temperatureInF - 32.0m) * 5.0m / 9.0m; + + var vaporPressure = relativeHumidity * 0.01m * 6.112m * DecimalEx.Exp(17.62m * temperatureInC / (temperatureInC + 243.12m)); + var numerator = 243.12m * DecimalEx.Log(vaporPressure) - 440.1m; + var denominator = 19.43m - DecimalEx.Log(vaporPressure); + var dewPointInC = numerator / denominator; + + DewPoint = dewPointInC * 9.0m / 5.0m + 32.0m; + } + + private void CalculatePressureTrend() + { + var pressureData = _database + .GetReadingValueHistory(WeatherValueType.Pressure, Timestamp.AddHours(-3), Timestamp).Result.ToList(); + + var xData = pressureData.Select(p => (double)p.Timestamp.ToUnixTimeSeconds()).ToArray(); + var yData = pressureData.Select(p => (double)p.Value / 100.0).ToArray(); + + var lineFunction = Fit.LineFunc(xData, yData); + + PressureDifferenceThreeHour = (decimal)(lineFunction(xData.Last()) - lineFunction(xData[0])); + } + + private void CalculateHeatIndex() + { + var temperature = Temperature; + var humidity = Humidity; + + if (temperature.IsBetween(80, 100) && humidity.IsBetween(40, 100)) + { + HeatIndex = -42.379m + 2.04901523m * temperature + 10.14333127m * humidity - + .22475541m * temperature * humidity - .00683783m * temperature * temperature - + .05481717m * humidity * humidity + .00122874m * temperature * temperature * humidity + + .00085282m * temperature * humidity * humidity - + .00000199m * temperature * temperature * humidity * humidity; + } + } +} \ No newline at end of file diff --git a/Weather/Service/Models/WeatherValue.cs b/Weather/Service/Models/WeatherValue.cs index 0b5200a..4e60c5c 100644 --- a/Weather/Service/Models/WeatherValue.cs +++ b/Weather/Service/Models/WeatherValue.cs @@ -1,13 +1,12 @@ using JetBrains.Annotations; using System; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Models -{ - [PublicAPI] - public class WeatherValue - { - public DateTimeOffset Timestamp { get; set; } +namespace ChrisKaczor.HomeMonitor.Weather.Service.Models; - public decimal Value { get; set; } - } -} +[PublicAPI] +public class WeatherValue +{ + public DateTimeOffset Timestamp { get; set; } + + public decimal Value { get; set; } +} \ No newline at end of file diff --git a/Weather/Service/Models/WeatherValueGrouped.cs b/Weather/Service/Models/WeatherValueGrouped.cs index c0b1daf..af0ddb7 100644 --- a/Weather/Service/Models/WeatherValueGrouped.cs +++ b/Weather/Service/Models/WeatherValueGrouped.cs @@ -1,13 +1,12 @@ using JetBrains.Annotations; using System; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Models -{ - [PublicAPI] - public class WeatherValueGrouped - { - public DateTimeOffset Bucket { get; set; } +namespace ChrisKaczor.HomeMonitor.Weather.Service.Models; - public decimal AverageValue { get; set; } - } +[PublicAPI] +public class WeatherValueGrouped +{ + public DateTimeOffset Bucket { get; set; } + + public decimal AverageValue { get; set; } } \ No newline at end of file diff --git a/Weather/Service/Models/WeatherValueType.cs b/Weather/Service/Models/WeatherValueType.cs index 53108c8..ef8cad0 100644 --- a/Weather/Service/Models/WeatherValueType.cs +++ b/Weather/Service/Models/WeatherValueType.cs @@ -1,18 +1,17 @@ using JetBrains.Annotations; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Models +namespace ChrisKaczor.HomeMonitor.Weather.Service.Models; + +[PublicAPI] +public enum WeatherValueType { - [PublicAPI] - public enum WeatherValueType - { - WindSpeed, - Humidity, - HumidityTemperature, - Rain, - Pressure, - PressureTemperature, - LightLevel, - Altitude, - SatelliteCount - } -} + WindSpeed, + Humidity, + HumidityTemperature, + Rain, + Pressure, + PressureTemperature, + LightLevel, + Altitude, + SatelliteCount +} \ No newline at end of file diff --git a/Weather/Service/Models/WindHistoryGrouped.cs b/Weather/Service/Models/WindHistoryGrouped.cs index 0dd0941..dbf13d5 100644 --- a/Weather/Service/Models/WindHistoryGrouped.cs +++ b/Weather/Service/Models/WindHistoryGrouped.cs @@ -1,19 +1,18 @@ using JetBrains.Annotations; using System; -namespace ChrisKaczor.HomeMonitor.Weather.Service.Models +namespace ChrisKaczor.HomeMonitor.Weather.Service.Models; + +[PublicAPI] +public class WindHistoryGrouped { - [PublicAPI] - public class WindHistoryGrouped - { - public DateTimeOffset Bucket { get; set; } + public DateTimeOffset Bucket { get; set; } - public decimal MinimumSpeed { get; set; } + public decimal MinimumSpeed { get; set; } - public decimal AverageSpeed { get; set; } + public decimal AverageSpeed { get; set; } - public decimal MaximumSpeed { get; set; } + public decimal MaximumSpeed { get; set; } - public decimal AverageDirection { get; set; } - } + public decimal AverageDirection { get; set; } } \ No newline at end of file diff --git a/Weather/Service/Program.cs b/Weather/Service/Program.cs index 6ac3aa9..d4808a4 100644 --- a/Weather/Service/Program.cs +++ b/Weather/Service/Program.cs @@ -1,19 +1,125 @@ -using Microsoft.AspNetCore; -using Microsoft.AspNetCore.Hosting; +using ChrisKaczor.HomeMonitor.Weather.Service.Data; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.ResponseCompression; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Exporter; +using OpenTelemetry.Logs; +using OpenTelemetry.Metrics; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using System; +using System.IO.Compression; +using System.Reflection; +using System.Text.Json.Serialization; -namespace ChrisKaczor.HomeMonitor.Weather.Service +namespace ChrisKaczor.HomeMonitor.Weather.Service; + +public static class Program { - public static class Program + public static void Main(string[] args) { - public static void Main(string[] args) - { - CreateWebHostBuilder(args).Build().Run(); - } + var builder = WebApplication.CreateBuilder(args); - private static IWebHostBuilder CreateWebHostBuilder(string[] args) + builder.Configuration.AddEnvironmentVariables(); + + builder.Services.AddControllers(); + + // --- + + var openTelemetry = builder.Services.AddOpenTelemetry(); + + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + + var serviceName = Assembly.GetExecutingAssembly().GetName().Name; + + openTelemetry.ConfigureResource(resource => resource.AddService(serviceName!)); + + openTelemetry.WithMetrics(meterProviderBuilder => meterProviderBuilder + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddProcessInstrumentation() + .AddMeter("Microsoft.AspNetCore.Hosting") + .AddMeter("Microsoft.AspNetCore.Server.Kestrel")); + + openTelemetry.WithTracing(tracerProviderBuilder => { - return WebHost.CreateDefaultBuilder(args).ConfigureAppConfiguration((_, config) => config.AddEnvironmentVariables()).UseStartup(); - } + tracerProviderBuilder.AddAspNetCoreInstrumentation(instrumentationOptions => instrumentationOptions.RecordException = true); + + tracerProviderBuilder.AddHttpClientInstrumentation(instrumentationOptions => instrumentationOptions.RecordException = true); + + tracerProviderBuilder.AddSqlClientInstrumentation(o => + { + o.RecordException = true; + o.SetDbStatementForText = true; + }); + + tracerProviderBuilder.AddSource(nameof(MessageHandler)); + + tracerProviderBuilder.SetErrorStatusOnException(); + + tracerProviderBuilder.AddOtlpExporter(exporterOptions => + { + exporterOptions.Endpoint = new Uri(builder.Configuration["Telemetry:Endpoint"]!); + exporterOptions.Protocol = OtlpExportProtocol.Grpc; + }); + }); + + builder.Services.AddLogging(loggingBuilder => + { + loggingBuilder.SetMinimumLevel(LogLevel.Information); + loggingBuilder.AddOpenTelemetry(options => + { + options.AddOtlpExporter(exporterOptions => + { + exporterOptions.Endpoint = new Uri(builder.Configuration["Telemetry:Endpoint"]!); + exporterOptions.Protocol = OtlpExportProtocol.Grpc; + }); + } + ); + }); + + builder.Services.AddTransient(); + + builder.Services.AddHostedService(); + + builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); + + builder.Services.AddResponseCompression(options => + { + options.Providers.Add(); + options.EnableForHttps = true; + }); + + builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", corsPolicyBuilder => corsPolicyBuilder.AllowAnyMethod().AllowAnyHeader().AllowCredentials().WithOrigins("http://localhost:4200"))); + + builder.Services.AddMvc().AddJsonOptions(configure => + { + configure.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + }); + + // --- + + var app = builder.Build(); + + if (builder.Environment.IsDevelopment()) + app.UseDeveloperExceptionPage(); + + var database = app.Services.GetRequiredService(); + database.EnsureDatabase(); + + app.UseCors("CorsPolicy"); + + app.UseResponseCompression(); + + app.UseRouting(); + + app.UseAuthorization(); + + app.MapControllers(); + + app.Run(); } } \ No newline at end of file diff --git a/Weather/Service/ResourceReader.cs b/Weather/Service/ResourceReader.cs index 9e25616..6a89d93 100644 --- a/Weather/Service/ResourceReader.cs +++ b/Weather/Service/ResourceReader.cs @@ -2,21 +2,20 @@ using System.IO; using System.Reflection; -namespace ChrisKaczor.HomeMonitor.Weather.Service +namespace ChrisKaczor.HomeMonitor.Weather.Service; + +public static class ResourceReader { - public static class ResourceReader + public static string GetString(string resourceName) { - public static string GetString(string resourceName) - { - var assembly = Assembly.GetExecutingAssembly(); + var assembly = Assembly.GetExecutingAssembly(); + using var stream = assembly.GetManifestResourceStream(resourceName); - using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + throw new Exception($"Resource {resourceName} not found in {assembly.FullName}. Valid resources are: {string.Join(", ", assembly.GetManifestResourceNames())}."); - if (stream == null) - throw new Exception($"Resource {resourceName} not found in {assembly.FullName}. Valid resources are: {string.Join(", ", assembly.GetManifestResourceNames())}."); + using var reader = new StreamReader(stream); - using var reader = new StreamReader(stream); - return reader.ReadToEnd(); - } + return reader.ReadToEnd(); } } \ No newline at end of file diff --git a/Weather/Service/Service.csproj b/Weather/Service/Service.csproj index c7c4ca8..4ff5077 100644 --- a/Weather/Service/Service.csproj +++ b/Weather/Service/Service.csproj @@ -1,15 +1,16 @@  - net7.0 + net8.0 InProcess ChrisKaczor.HomeMonitor.Weather.Service ChrisKaczor.HomeMonitor.Weather.Service - ../../ChrisKaczor.ruleset false + + @@ -35,12 +36,17 @@ - + - - - - + + + + + + + + + diff --git a/Weather/Service/Startup.cs b/Weather/Service/Startup.cs deleted file mode 100644 index 4c261a2..0000000 --- a/Weather/Service/Startup.cs +++ /dev/null @@ -1,54 +0,0 @@ -using ChrisKaczor.HomeMonitor.Weather.Service.Data; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.ResponseCompression; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using System.IO.Compression; -using System.Text.Json.Serialization; - -namespace ChrisKaczor.HomeMonitor.Weather.Service -{ - public class Startup - { - public void ConfigureServices(IServiceCollection services) - { - services.AddTransient(); - - services.AddHostedService(); - - services.Configure(options => options.Level = CompressionLevel.Optimal); - - services.AddResponseCompression(options => - { - options.Providers.Add(); - options.EnableForHttps = true; - }); - - services.AddCors(o => o.AddPolicy("CorsPolicy", builder => builder.AllowAnyMethod().AllowAnyHeader().AllowCredentials().WithOrigins("http://localhost:4200"))); - - services.AddMvc().AddJsonOptions(configure => - { - configure.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); - }); - } - - public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment environment) - { - if (environment.IsDevelopment()) - applicationBuilder.UseDeveloperExceptionPage(); - - var database = applicationBuilder.ApplicationServices.GetService(); - database.EnsureDatabase(); - - applicationBuilder.UseCors("CorsPolicy"); - - applicationBuilder.UseResponseCompression(); - - applicationBuilder.UseRouting(); - - applicationBuilder.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute()); - } - } -} \ No newline at end of file diff --git a/Weather/Service/appsettings.Development.json b/Weather/Service/appsettings.Development.json index 7cf4a21..dc96fb4 100644 --- a/Weather/Service/appsettings.Development.json +++ b/Weather/Service/appsettings.Development.json @@ -2,7 +2,6 @@ "Logging": { "LogLevel": { "Default": "Debug", - "System": "Information", "Microsoft": "Information" } } diff --git a/Weather/Service/appsettings.json b/Weather/Service/appsettings.json index 4ad3651..0c90fbd 100644 --- a/Weather/Service/appsettings.json +++ b/Weather/Service/appsettings.json @@ -1,12 +1,14 @@ { "Logging": { "LogLevel": { - "Default": "Warning" + "Default": "Warning", + "Microsoft": "Information" } }, "Weather": { "Database": { - "Name": "Weather" + "Name": "Weather", + "TrustServerCertificate": true }, "Queue": { "Name": "weather" @@ -14,5 +16,8 @@ }, "Hub": { "Weather": "http://hub-server/weather" + }, + "Telemetry": { + "Endpoint": "http://signoz-otel-collector.platform:4317/" } } \ No newline at end of file diff --git a/Weather/Service/deploy/manifest.yaml b/Weather/Service/deploy/manifest.yaml index 5e787cb..7b10abf 100644 --- a/Weather/Service/deploy/manifest.yaml +++ b/Weather/Service/deploy/manifest.yaml @@ -132,7 +132,7 @@ metadata: spec: ports: - name: client - port: 80 + port: 8080 selector: app: weather-service type: ClusterIP @@ -156,7 +156,7 @@ spec: - kind: Service name: weather-service namespace: home-monitor - port: 80 + port: 8080 --- apiVersion: traefik.containo.us/v1alpha1 kind: Middleware