mirror of
https://github.com/ckaczor/HomeMonitor.git
synced 2026-01-14 01:25:38 -05:00
Power service updates
- Upgrade to .NET 8 - Remove ApplicationInsights - Add OpenTelemetry
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 16
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 16.0.28705.295
|
VisualStudioVersion = 16.0.28705.295
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.csproj", "{914B9DB9-3BCD-4B55-8289-2E59D6CA96BA}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "Service\Service.csproj", "{914B9DB9-3BCD-4B55-8289-2E59D6CA96BA}"
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|||||||
@@ -6,29 +6,21 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service.Controllers
|
namespace ChrisKaczor.HomeMonitor.Power.Service.Controllers;
|
||||||
|
|
||||||
|
[Route("[controller]")]
|
||||||
|
[ApiController]
|
||||||
|
public class StatusController(Database database) : ControllerBase
|
||||||
{
|
{
|
||||||
[Route("[controller]")]
|
[HttpGet("recent")]
|
||||||
[ApiController]
|
public async Task<ActionResult<PowerStatus>> GetRecent()
|
||||||
public class StatusController : ControllerBase
|
|
||||||
{
|
{
|
||||||
private readonly Database _database;
|
return await database.GetRecentStatus();
|
||||||
|
}
|
||||||
|
|
||||||
public StatusController(Database database)
|
[HttpGet("history-grouped")]
|
||||||
{
|
public async Task<ActionResult<List<PowerStatusGrouped>>> GetHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes = 2)
|
||||||
_database = database;
|
{
|
||||||
}
|
return (await database.GetStatusHistoryGrouped(start, end, bucketMinutes)).ToList();
|
||||||
|
|
||||||
[HttpGet("recent")]
|
|
||||||
public async Task<ActionResult<PowerStatus>> GetRecent()
|
|
||||||
{
|
|
||||||
return await _database.GetRecentStatus();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("history-grouped")]
|
|
||||||
public async Task<ActionResult<List<PowerStatusGrouped>>> GetHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes = 2)
|
|
||||||
{
|
|
||||||
return (await _database.GetStatusHistoryGrouped(start, end, bucketMinutes)).ToList();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,95 +6,89 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service.Data
|
namespace ChrisKaczor.HomeMonitor.Power.Service.Data;
|
||||||
|
|
||||||
|
public class Database(IConfiguration configuration)
|
||||||
{
|
{
|
||||||
public class Database
|
public void EnsureDatabase()
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
var connectionStringBuilder = new SqlConnectionStringBuilder
|
||||||
|
|
||||||
public Database(IConfiguration configuration)
|
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
DataSource = $"{configuration["Power:Database:Host"]},{configuration["Power:Database:Port"]}",
|
||||||
}
|
UserID = configuration["Power:Database:User"],
|
||||||
|
Password = configuration["Power:Database:Password"],
|
||||||
|
InitialCatalog = "master",
|
||||||
|
TrustServerCertificate = bool.Parse(configuration["Power:Database:TrustServerCertificate"] ?? "false")
|
||||||
|
};
|
||||||
|
|
||||||
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["Power:Database:Name"]}'";
|
||||||
|
var databaseExists = (bool?)command.ExecuteScalar();
|
||||||
|
|
||||||
|
// Create database if needed
|
||||||
|
if (!(databaseExists ?? false))
|
||||||
{
|
{
|
||||||
var connectionStringBuilder = new SqlConnectionStringBuilder
|
command.CommandText = $"CREATE DATABASE {configuration["Power:Database:Name"]}";
|
||||||
{
|
|
||||||
DataSource = $"{_configuration["Power:Database:Host"]},{_configuration["Power:Database:Port"]}",
|
|
||||||
UserID = _configuration["Power:Database:User"],
|
|
||||||
Password = _configuration["Power: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["Power:Database:Name"]}'";
|
|
||||||
var databaseExists = (bool?)command.ExecuteScalar();
|
|
||||||
|
|
||||||
// Create database if needed
|
|
||||||
if (!(databaseExists ?? false))
|
|
||||||
{
|
|
||||||
command.CommandText = $"CREATE DATABASE {_configuration["Power:Database:Name"]}";
|
|
||||||
command.ExecuteNonQuery();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Switch to the database now that we're sure it exists
|
|
||||||
connection.ChangeDatabase(_configuration["Power:Database:Name"]);
|
|
||||||
|
|
||||||
var schema = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.Schema.sql");
|
|
||||||
|
|
||||||
// Make sure the database is up to date
|
|
||||||
command.CommandText = schema;
|
|
||||||
command.ExecuteNonQuery();
|
command.ExecuteNonQuery();
|
||||||
}
|
}
|
||||||
|
|
||||||
private SqlConnection CreateConnection()
|
// Switch to the database now that we're sure it exists
|
||||||
|
connection.ChangeDatabase(configuration["Power:Database:Name"]!);
|
||||||
|
|
||||||
|
var schema = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.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["Power:Database:Host"]},{configuration["Power:Database:Port"]}",
|
||||||
{
|
UserID = configuration["Power:Database:User"],
|
||||||
DataSource = $"{_configuration["Power:Database:Host"]},{_configuration["Power:Database:Port"]}",
|
Password = configuration["Power:Database:Password"],
|
||||||
UserID = _configuration["Power:Database:User"],
|
InitialCatalog = configuration["Power:Database:Name"],
|
||||||
Password = _configuration["Power:Database:Password"],
|
TrustServerCertificate = bool.Parse(configuration["Power:Database:TrustServerCertificate"] ?? "false")
|
||||||
InitialCatalog = _configuration["Power:Database:Name"]
|
};
|
||||||
};
|
|
||||||
|
|
||||||
var connection = new SqlConnection(connectionStringBuilder.ConnectionString);
|
var connection = new SqlConnection(connectionStringBuilder.ConnectionString);
|
||||||
connection.Open();
|
connection.Open();
|
||||||
|
|
||||||
return connection;
|
return connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void StorePowerData(PowerStatus powerStatus)
|
public void StorePowerData(PowerStatus powerStatus)
|
||||||
{
|
{
|
||||||
using var connection = CreateConnection();
|
using var connection = CreateConnection();
|
||||||
|
|
||||||
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.CreateStatus.sql");
|
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.CreateStatus.sql");
|
||||||
|
|
||||||
connection.Query(query, powerStatus);
|
connection.Query(query, powerStatus);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<PowerStatus> GetRecentStatus()
|
public async Task<PowerStatus> GetRecentStatus()
|
||||||
{
|
{
|
||||||
await using var connection = CreateConnection();
|
await using var connection = CreateConnection();
|
||||||
|
|
||||||
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.GetRecentStatus.sql");
|
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.GetRecentStatus.sql");
|
||||||
|
|
||||||
return await connection.QueryFirstOrDefaultAsync<PowerStatus>(query);
|
return await connection.QueryFirstOrDefaultAsync<PowerStatus>(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IEnumerable<PowerStatusGrouped>> GetStatusHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes)
|
public async Task<IEnumerable<PowerStatusGrouped>> GetStatusHistoryGrouped(DateTimeOffset start, DateTimeOffset end, int bucketMinutes)
|
||||||
{
|
{
|
||||||
await using var connection = CreateConnection();
|
await using var connection = CreateConnection();
|
||||||
|
|
||||||
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.GetStatusHistoryGrouped.sql");
|
var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Power.Service.Data.Resources.GetStatusHistoryGrouped.sql");
|
||||||
|
|
||||||
return await connection.QueryAsync<PowerStatusGrouped>(query, new { Start = start, End = end, BucketMinutes = bucketMinutes });
|
return await connection.QueryAsync<PowerStatusGrouped>(query, new { Start = start, End = end, BucketMinutes = bucketMinutes });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
FROM mcr.microsoft.com/dotnet/aspnet:5.0-alpine AS base
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
EXPOSE 80
|
EXPOSE 80
|
||||||
|
|
||||||
FROM mcr.microsoft.com/dotnet/sdk:5.0-alpine AS build
|
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
COPY ["./Service.csproj", "./"]
|
COPY ["./Service.csproj", "./"]
|
||||||
RUN dotnet restore "Service.csproj"
|
RUN dotnet restore "Service.csproj"
|
||||||
|
|||||||
@@ -1,30 +1,29 @@
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service.Models
|
namespace ChrisKaczor.HomeMonitor.Power.Service.Models;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public class PowerChannel
|
||||||
{
|
{
|
||||||
[PublicAPI]
|
[JsonPropertyName("type")]
|
||||||
public class PowerChannel
|
public string Type { get; set; }
|
||||||
{
|
|
||||||
[JsonPropertyName("type")]
|
|
||||||
public string Type { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("ch")]
|
[JsonPropertyName("ch")]
|
||||||
public long ChannelNumber { get; set; }
|
public long ChannelNumber { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("eImp_Ws")]
|
[JsonPropertyName("eImp_Ws")]
|
||||||
public long ImportedEnergy { get; set; }
|
public long ImportedEnergy { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("eExp_Ws")]
|
[JsonPropertyName("eExp_Ws")]
|
||||||
public long ExportedEnergy { get; set; }
|
public long ExportedEnergy { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("p_W")]
|
[JsonPropertyName("p_W")]
|
||||||
public long RealPower { get; set; }
|
public long RealPower { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("q_VAR")]
|
[JsonPropertyName("q_VAR")]
|
||||||
public long ReactivePower { get; set; }
|
public long ReactivePower { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("v_V")]
|
[JsonPropertyName("v_V")]
|
||||||
public double Voltage { get; set; }
|
public double Voltage { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,21 +3,20 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service.Models
|
namespace ChrisKaczor.HomeMonitor.Power.Service.Models;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public class PowerSample
|
||||||
{
|
{
|
||||||
[PublicAPI]
|
[JsonPropertyName("sensorId")]
|
||||||
public class PowerSample
|
public string SensorId { get; set; }
|
||||||
{
|
|
||||||
[JsonPropertyName("sensorId")]
|
|
||||||
public string SensorId { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("timestamp")]
|
[JsonPropertyName("timestamp")]
|
||||||
public DateTimeOffset Timestamp { get; set; }
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("channels")]
|
[JsonPropertyName("channels")]
|
||||||
public PowerChannel[] Channels { get; set; }
|
public PowerChannel[] Channels { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("cts")]
|
[JsonPropertyName("cts")]
|
||||||
public Dictionary<string, double>[] CurrentTransformers { get; set; }
|
public Dictionary<string, double>[] CurrentTransformers { get; set; }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service.Models
|
namespace ChrisKaczor.HomeMonitor.Power.Service.Models;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public class PowerStatus
|
||||||
{
|
{
|
||||||
[PublicAPI]
|
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
|
||||||
public class PowerStatus
|
public long Generation { get; set; }
|
||||||
{
|
public long Consumption { get; set; }
|
||||||
public DateTimeOffset Timestamp { get; set; } = DateTimeOffset.UtcNow;
|
|
||||||
public long Generation { get; set; }
|
|
||||||
public long Consumption { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service.Models
|
namespace ChrisKaczor.HomeMonitor.Power.Service.Models;
|
||||||
|
|
||||||
|
[PublicAPI]
|
||||||
|
public class PowerStatusGrouped
|
||||||
{
|
{
|
||||||
[PublicAPI]
|
public DateTimeOffset Bucket { get; set; }
|
||||||
public class PowerStatusGrouped
|
public long AverageGeneration { get; set; }
|
||||||
{
|
public long AverageConsumption { get; set; }
|
||||||
public DateTimeOffset Bucket { get; set; }
|
|
||||||
public long AverageGeneration { get; set; }
|
|
||||||
public long AverageConsumption { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,102 +1,94 @@
|
|||||||
using ChrisKaczor.HomeMonitor.Power.Service.Data;
|
using ChrisKaczor.HomeMonitor.Power.Service.Data;
|
||||||
using ChrisKaczor.HomeMonitor.Power.Service.Models;
|
using ChrisKaczor.HomeMonitor.Power.Service.Models;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.ApplicationInsights;
|
|
||||||
using Microsoft.AspNetCore.SignalR.Client;
|
using Microsoft.AspNetCore.SignalR.Client;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using RestSharp;
|
using RestSharp;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service
|
namespace ChrisKaczor.HomeMonitor.Power.Service;
|
||||||
|
|
||||||
|
[UsedImplicitly]
|
||||||
|
public class PowerReader(IConfiguration configuration, Database database, ILogger<PowerReader> logger) : IHostedService
|
||||||
{
|
{
|
||||||
[UsedImplicitly]
|
private readonly ActivitySource _activitySource = new(nameof(PowerReader));
|
||||||
public class PowerReader : IHostedService
|
|
||||||
|
private HubConnection _hubConnection;
|
||||||
|
private Timer _readTimer;
|
||||||
|
|
||||||
|
public Task StartAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
private readonly IConfiguration _configuration;
|
logger.LogInformation($"{nameof(PowerReader)} - Start");
|
||||||
private readonly Database _database;
|
|
||||||
private readonly TelemetryClient _telemetryClient;
|
|
||||||
|
|
||||||
private HubConnection _hubConnection;
|
_readTimer = new Timer(GetCurrentSample, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
|
||||||
private Timer _readTimer;
|
|
||||||
|
|
||||||
public PowerReader(IConfiguration configuration, Database database, TelemetryClient telemetryClient)
|
if (!string.IsNullOrEmpty(configuration["Hub:Power"]))
|
||||||
|
_hubConnection = new HubConnectionBuilder().WithUrl(configuration["Hub:Power"]).Build();
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void GetCurrentSample(object state)
|
||||||
|
{
|
||||||
|
using var activity = _activitySource.StartActivity();
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_configuration = configuration;
|
var client = new RestClient(configuration["Power:Host"]!);
|
||||||
_database = database;
|
|
||||||
_telemetryClient = telemetryClient;
|
var request = new RestRequest("current-sample");
|
||||||
|
request.AddHeader("Authorization", configuration["Power:AuthorizationHeader"]!);
|
||||||
|
|
||||||
|
var response = client.Execute(request);
|
||||||
|
|
||||||
|
var content = response.Content!;
|
||||||
|
|
||||||
|
logger.LogInformation("API response: {content}", content);
|
||||||
|
|
||||||
|
var sample = JsonSerializer.Deserialize<PowerSample>(content);
|
||||||
|
|
||||||
|
var generation = Array.Find(sample.Channels, c => c.Type == "GENERATION");
|
||||||
|
var consumption = Array.Find(sample.Channels, c => c.Type == "CONSUMPTION");
|
||||||
|
|
||||||
|
if (generation == null || consumption == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var status = new PowerStatus { Generation = generation.RealPower, Consumption = consumption.RealPower };
|
||||||
|
|
||||||
|
database.StorePowerData(status);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(status);
|
||||||
|
|
||||||
|
logger.LogInformation("Output message: {json}", json);
|
||||||
|
|
||||||
|
if (_hubConnection == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (_hubConnection.State == HubConnectionState.Disconnected)
|
||||||
|
_hubConnection.StartAsync().Wait();
|
||||||
|
|
||||||
|
_hubConnection.InvokeAsync("SendLatestSample", json).Wait();
|
||||||
}
|
}
|
||||||
|
catch (Exception exception)
|
||||||
public Task StartAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
_telemetryClient.TrackTrace($"{nameof(PowerReader)} - Start");
|
logger.LogError(exception, "Exception");
|
||||||
|
|
||||||
_readTimer = new Timer(OnTimer, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(_configuration["Hub:Power"]))
|
|
||||||
_hubConnection = new HubConnectionBuilder().WithUrl(_configuration["Hub:Power"]).Build();
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnTimer(object state)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var client = new RestClient(_configuration["Power:Host"]);
|
|
||||||
|
|
||||||
var request = new RestRequest("current-sample", Method.GET);
|
|
||||||
request.AddHeader("Authorization", _configuration["Power:AuthorizationHeader"]);
|
|
||||||
|
|
||||||
var response = client.Execute(request);
|
|
||||||
|
|
||||||
var sample = JsonSerializer.Deserialize<PowerSample>(response.Content);
|
|
||||||
|
|
||||||
var generation = Array.Find(sample.Channels, c => c.Type == "GENERATION");
|
|
||||||
var consumption = Array.Find(sample.Channels, c => c.Type == "CONSUMPTION");
|
|
||||||
|
|
||||||
if (generation == null || consumption == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var status = new PowerStatus { Generation = generation.RealPower, Consumption = consumption.RealPower };
|
|
||||||
|
|
||||||
_database.StorePowerData(status);
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(status);
|
|
||||||
|
|
||||||
Console.WriteLine(json);
|
|
||||||
|
|
||||||
if (_hubConnection == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (_hubConnection.State == HubConnectionState.Disconnected)
|
|
||||||
_hubConnection.StartAsync().Wait();
|
|
||||||
|
|
||||||
_hubConnection.InvokeAsync("SendLatestSample", json).Wait();
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
WriteLog($"Exception: {exception}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_telemetryClient.TrackTrace($"{nameof(PowerReader)} - Stop");
|
|
||||||
|
|
||||||
_readTimer.Dispose();
|
|
||||||
|
|
||||||
_hubConnection?.StopAsync(cancellationToken).Wait(cancellationToken);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void WriteLog(string message)
|
|
||||||
{
|
|
||||||
Console.WriteLine(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task StopAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
logger.LogInformation($"{nameof(PowerReader)} - Stop");
|
||||||
|
|
||||||
|
_readTimer.Dispose();
|
||||||
|
|
||||||
|
_hubConnection?.StopAsync(cancellationToken).Wait(cancellationToken);
|
||||||
|
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,127 @@
|
|||||||
using Microsoft.AspNetCore;
|
using ChrisKaczor.HomeMonitor.Power.Service.Data;
|
||||||
using Microsoft.AspNetCore.Hosting;
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.ResponseCompression;
|
||||||
using Microsoft.Extensions.Configuration;
|
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;
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service
|
namespace ChrisKaczor.HomeMonitor.Power.Service;
|
||||||
|
|
||||||
|
public static class Program
|
||||||
{
|
{
|
||||||
public static class Program
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
{
|
|
||||||
CreateWebHostBuilder(args).Build().Run();
|
|
||||||
}
|
|
||||||
|
|
||||||
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 name = Assembly.GetExecutingAssembly().GetName().Name;
|
||||||
|
|
||||||
|
openTelemetry.ConfigureResource(resource => resource.AddService(name!));
|
||||||
|
|
||||||
|
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<Startup>();
|
tracerProviderBuilder.AddAspNetCoreInstrumentation(instrumentationOptions => instrumentationOptions.RecordException = true);
|
||||||
}
|
|
||||||
|
tracerProviderBuilder.AddHttpClientInstrumentation(instrumentationOptions => instrumentationOptions.RecordException = true);
|
||||||
|
|
||||||
|
tracerProviderBuilder.AddSqlClientInstrumentation(o =>
|
||||||
|
{
|
||||||
|
o.RecordException = true;
|
||||||
|
o.SetDbStatementForText = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
tracerProviderBuilder.AddSource(nameof(PowerReader));
|
||||||
|
|
||||||
|
if (builder.Environment.IsDevelopment())
|
||||||
|
tracerProviderBuilder.AddConsoleExporter();
|
||||||
|
|
||||||
|
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 =>
|
||||||
|
{
|
||||||
|
if (builder.Environment.IsDevelopment())
|
||||||
|
options.AddConsoleExporter();
|
||||||
|
|
||||||
|
options.AddOtlpExporter(exporterOptions =>
|
||||||
|
{
|
||||||
|
exporterOptions.Endpoint = new Uri(builder.Configuration["Telemetry:Endpoint"]!);
|
||||||
|
exporterOptions.Protocol = OtlpExportProtocol.Grpc;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddTransient<Database>();
|
||||||
|
|
||||||
|
builder.Services.AddHostedService<PowerReader>();
|
||||||
|
|
||||||
|
builder.Services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Optimal);
|
||||||
|
|
||||||
|
builder.Services.AddResponseCompression(options =>
|
||||||
|
{
|
||||||
|
options.Providers.Add<GzipCompressionProvider>();
|
||||||
|
options.EnableForHttps = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddCors(o => o.AddPolicy("CorsPolicy", corsPolicyBuilder => corsPolicyBuilder.AllowAnyMethod().AllowAnyHeader().AllowCredentials().WithOrigins("http://localhost:4200")));
|
||||||
|
|
||||||
|
builder.Services.AddMvc();
|
||||||
|
|
||||||
|
// ---
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
if (builder.Environment.IsDevelopment())
|
||||||
|
app.UseDeveloperExceptionPage();
|
||||||
|
|
||||||
|
var database = app.Services.GetRequiredService<Database>();
|
||||||
|
database.EnsureDatabase();
|
||||||
|
|
||||||
|
app.UseCors("CorsPolicy");
|
||||||
|
|
||||||
|
app.UseResponseCompression();
|
||||||
|
|
||||||
|
app.UseRouting();
|
||||||
|
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
app.Run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,21 +2,20 @@
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service
|
namespace ChrisKaczor.HomeMonitor.Power.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();
|
||||||
{
|
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
var assembly = Assembly.GetExecutingAssembly();
|
|
||||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
|
||||||
|
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
throw new Exception($"Resource {resourceName} not found in {assembly.FullName}. Valid resources are: {string.Join(", ", assembly.GetManifestResourceNames())}.");
|
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();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,36 +1,35 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net5.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||||
<AssemblyName>ChrisKaczor.HomeMonitor.Power.Service</AssemblyName>
|
<AssemblyName>ChrisKaczor.HomeMonitor.Power.Service</AssemblyName>
|
||||||
<RootNamespace>ChrisKaczor.HomeMonitor.Power.Service</RootNamespace>
|
<RootNamespace>ChrisKaczor.HomeMonitor.Power.Service</RootNamespace>
|
||||||
<CodeAnalysisRuleSet>../../ChrisKaczor.ruleset</CodeAnalysisRuleSet>
|
|
||||||
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
<GenerateTargetFrameworkAttribute>false</GenerateTargetFrameworkAttribute>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<None Remove="C:\Users\chris\.nuget\packages\opentelemetry.autoinstrumentation\1.3.0\contentFiles\any\any\instrument.cmd" />
|
||||||
|
<None Remove="C:\Users\chris\.nuget\packages\opentelemetry.autoinstrumentation\1.3.0\contentFiles\any\any\instrument.sh" />
|
||||||
<None Remove="Data\Resources\CreateStatus.sql" />
|
<None Remove="Data\Resources\CreateStatus.sql" />
|
||||||
<None Remove="Data\Resources\GetStatusHistoryGrouped.sql" />
|
<None Remove="Data\Resources\GetStatusHistoryGrouped.sql" />
|
||||||
<None Remove="Data\Resources\Schema.sql" />
|
<None Remove="Data\Resources\Schema.sql" />
|
||||||
<None Remove="Data\Resources\GetRecentStatus.sql" />
|
<None Remove="Data\Resources\GetRecentStatus.sql" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Data\Resources\CreateStatus.sql" />
|
<EmbeddedResource Include="Data\Resources\CreateStatus.sql" />
|
||||||
<EmbeddedResource Include="Data\Resources\GetStatusHistoryGrouped.sql" />
|
<EmbeddedResource Include="Data\Resources\GetStatusHistoryGrouped.sql" />
|
||||||
<EmbeddedResource Include="Data\Resources\Schema.sql" />
|
<EmbeddedResource Include="Data\Resources\Schema.sql" />
|
||||||
<EmbeddedResource Include="Data\Resources\GetRecentStatus.sql" />
|
<EmbeddedResource Include="Data\Resources\GetRecentStatus.sql" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Dapper" Version="2.0.90" />
|
<PackageReference Include="Dapper" Version="2.1.28" />
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2021.1.0" />
|
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
|
||||||
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.17.0" />
|
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.1.4" />
|
||||||
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.17.0" />
|
<PackageReference Include="OpenTelemetry.AutoInstrumentation" Version="1.3.0" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.6" />
|
<PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.7.0" />
|
||||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="2.1.3" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.7.0" />
|
||||||
<PackageReference Include="RestSharp" Version="106.11.7" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.7.0" />
|
||||||
|
<PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.7.0" />
|
||||||
|
<PackageReference Include="RestSharp" Version="110.2.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using ChrisKaczor.HomeMonitor.Power.Service.Data;
|
|
||||||
using Microsoft.ApplicationInsights;
|
|
||||||
using Microsoft.ApplicationInsights.Extensibility;
|
|
||||||
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.Threading;
|
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service
|
|
||||||
{
|
|
||||||
public class Startup
|
|
||||||
{
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddSingleton<ITelemetryInitializer, TelemetryInitializer>();
|
|
||||||
|
|
||||||
services.AddApplicationInsightsTelemetry(options =>
|
|
||||||
{
|
|
||||||
options.EnableDependencyTrackingTelemetryModule = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddTransient<Database>();
|
|
||||||
|
|
||||||
services.AddHostedService<PowerReader>();
|
|
||||||
|
|
||||||
services.Configure<GzipCompressionProviderOptions>(options => options.Level = CompressionLevel.Optimal);
|
|
||||||
|
|
||||||
services.AddResponseCompression(options =>
|
|
||||||
{
|
|
||||||
options.Providers.Add<GzipCompressionProvider>();
|
|
||||||
options.EnableForHttps = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddCors(o => o.AddPolicy("CorsPolicy", builder => builder.AllowAnyMethod().AllowAnyHeader().AllowCredentials().WithOrigins("http://localhost:4200")));
|
|
||||||
|
|
||||||
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_3_0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment environment, IHostApplicationLifetime hostApplicationLifetime)
|
|
||||||
{
|
|
||||||
if (environment.IsDevelopment())
|
|
||||||
applicationBuilder.UseDeveloperExceptionPage();
|
|
||||||
|
|
||||||
hostApplicationLifetime.ApplicationStopping.Register(() =>
|
|
||||||
{
|
|
||||||
var telemetryClient = applicationBuilder.ApplicationServices.GetRequiredService<TelemetryClient>();
|
|
||||||
|
|
||||||
telemetryClient.Flush();
|
|
||||||
|
|
||||||
Thread.Sleep(5000);
|
|
||||||
});
|
|
||||||
|
|
||||||
var database = applicationBuilder.ApplicationServices.GetRequiredService<Database>();
|
|
||||||
database.EnsureDatabase();
|
|
||||||
|
|
||||||
applicationBuilder.UseCors("CorsPolicy");
|
|
||||||
|
|
||||||
applicationBuilder.UseResponseCompression();
|
|
||||||
|
|
||||||
applicationBuilder.UseRouting();
|
|
||||||
|
|
||||||
applicationBuilder.UseEndpoints(endpoints => endpoints.MapDefaultControllerRoute());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using Microsoft.ApplicationInsights.Channel;
|
|
||||||
using Microsoft.ApplicationInsights.Extensibility;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace ChrisKaczor.HomeMonitor.Power.Service
|
|
||||||
{
|
|
||||||
public class TelemetryInitializer : ITelemetryInitializer
|
|
||||||
{
|
|
||||||
public void Initialize(ITelemetry telemetry)
|
|
||||||
{
|
|
||||||
telemetry.Context.Cloud.RoleName = Assembly.GetEntryAssembly()?.GetName().Name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
{
|
{
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Warning"
|
"Default": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Power": {
|
||||||
|
"Database": {
|
||||||
|
"Name": "Power",
|
||||||
|
"Port": 1434,
|
||||||
|
"TrustServerCertificate": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Hub": {
|
||||||
|
"Power": ""
|
||||||
|
},
|
||||||
|
"Telemetry": {
|
||||||
|
"Endpoint": "http://signoz-otel-collector.platform:4317/"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Power": {
|
|
||||||
"Database": {
|
|
||||||
"Name": "Power",
|
|
||||||
"Port": 1434
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Hub": {
|
|
||||||
"Power": "http://hub-server/power"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -94,11 +94,6 @@ spec:
|
|||||||
securityContext:
|
securityContext:
|
||||||
privileged: true
|
privileged: true
|
||||||
env:
|
env:
|
||||||
- name: ApplicationInsights__ConnectionString
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: telemetry
|
|
||||||
key: key
|
|
||||||
- name: Power__Database__Host
|
- name: Power__Database__Host
|
||||||
value: power-database
|
value: power-database
|
||||||
- name: Power__Database__User
|
- name: Power__Database__User
|
||||||
|
|||||||
Reference in New Issue
Block a user