From 43ae1061f87f3a020a066800e0b629ac92cc4c90 Mon Sep 17 00:00:00 2001 From: Chris Kaczor Date: Sun, 13 Oct 2019 09:18:57 -0400 Subject: [PATCH] Initial framework of the power service --- Power/Power.sln | 33 ++++ Power/Service/.vscode/launch.json | 30 ++++ Power/Service/.vscode/tasks.json | 35 +++++ .../Service/Controllers/ReadingsController.cs | 11 ++ Power/Service/Dockerfile | 16 ++ Power/Service/Models/PowerChannel.cs | 30 ++++ Power/Service/Models/PowerSample.cs | 21 +++ Power/Service/PowerReader.cs | 81 ++++++++++ Power/Service/Program.cs | 22 +++ Power/Service/Properties/launchSettings.json | 13 ++ Power/Service/Service.csproj | 17 +++ Power/Service/Service.csproj.user | 9 ++ Power/Service/Startup.cs | 50 ++++++ Power/Service/appsettings.json | 10 ++ Power/Service/deploy/azure-pipelines.yml | 43 ++++++ Power/Service/deploy/manifest.yaml | 144 ++++++++++++++++++ 16 files changed, 565 insertions(+) create mode 100644 Power/Power.sln create mode 100644 Power/Service/.vscode/launch.json create mode 100644 Power/Service/.vscode/tasks.json create mode 100644 Power/Service/Controllers/ReadingsController.cs create mode 100644 Power/Service/Dockerfile create mode 100644 Power/Service/Models/PowerChannel.cs create mode 100644 Power/Service/Models/PowerSample.cs create mode 100644 Power/Service/PowerReader.cs create mode 100644 Power/Service/Program.cs create mode 100644 Power/Service/Properties/launchSettings.json create mode 100644 Power/Service/Service.csproj create mode 100644 Power/Service/Service.csproj.user create mode 100644 Power/Service/Startup.cs create mode 100644 Power/Service/appsettings.json create mode 100644 Power/Service/deploy/azure-pipelines.yml create mode 100644 Power/Service/deploy/manifest.yaml diff --git a/Power/Power.sln b/Power/Power.sln new file mode 100644 index 0000000..a25aa10 --- /dev/null +++ b/Power/Power.sln @@ -0,0 +1,33 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.28705.295 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.csproj", "{914B9DB9-3BCD-4B55-8289-2E59D6CA96BA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {63142748-213D-4BD3-9A15-8E5C405718B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63142748-213D-4BD3-9A15-8E5C405718B4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63142748-213D-4BD3-9A15-8E5C405718B4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63142748-213D-4BD3-9A15-8E5C405718B4}.Release|Any CPU.Build.0 = Release|Any CPU + {39E5DE26-CD8D-47AF-AB94-6ACB0AF24EA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {39E5DE26-CD8D-47AF-AB94-6ACB0AF24EA1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {39E5DE26-CD8D-47AF-AB94-6ACB0AF24EA1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {39E5DE26-CD8D-47AF-AB94-6ACB0AF24EA1}.Release|Any CPU.Build.0 = Release|Any CPU + {914B9DB9-3BCD-4B55-8289-2E59D6CA96BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {914B9DB9-3BCD-4B55-8289-2E59D6CA96BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {914B9DB9-3BCD-4B55-8289-2E59D6CA96BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {914B9DB9-3BCD-4B55-8289-2E59D6CA96BA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7E56B149-7297-42A1-9607-6B611D7EB09B} + EndGlobalSection +EndGlobal diff --git a/Power/Service/.vscode/launch.json b/Power/Service/.vscode/launch.json new file mode 100644 index 0000000..818819d --- /dev/null +++ b/Power/Service/.vscode/launch.json @@ -0,0 +1,30 @@ +{ + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Core Launch (remote console)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "publish", + "program": "dotnet", + "args": [ + "/home/ckaczor/Power/Service/Power.Service.dll" + ], + "cwd": "/home/ckaczor/Power/Service", + "stopAtEntry": false, + "console": "internalConsole", + "pipeTransport": { + "pipeCwd": "${workspaceFolder}", + "pipeProgram": "C:\\Program Files (x86)\\PuTTY\\PLINK.EXE", + "pipeArgs": [ + "root@172.23.10.6" + ], + "debuggerPath": "/home/ckaczor/vsdbg/vsdbg" + }, + "internalConsoleOptions": "openOnSessionStart" + } + ] +} \ No newline at end of file diff --git a/Power/Service/.vscode/tasks.json b/Power/Service/.vscode/tasks.json new file mode 100644 index 0000000..e872c92 --- /dev/null +++ b/Power/Service/.vscode/tasks.json @@ -0,0 +1,35 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/Service.csproj" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "publish", + "type": "shell", + "presentation": { + "reveal": "always", + "panel": "shared" + }, + "options": { + "cwd": "${workspaceFolder}" + }, + "windows": { + "command": "${cwd}\\publish.bat" + }, + "problemMatcher": [], + "group": "build" + } + ] +} \ No newline at end of file diff --git a/Power/Service/Controllers/ReadingsController.cs b/Power/Service/Controllers/ReadingsController.cs new file mode 100644 index 0000000..dc48dc7 --- /dev/null +++ b/Power/Service/Controllers/ReadingsController.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Mvc; + +namespace ChrisKaczor.HomeMonitor.Power.Service.Controllers +{ + [Route("[controller]")] + [ApiController] + public class ReadingsController : ControllerBase + { + + } +} \ No newline at end of file diff --git a/Power/Service/Dockerfile b/Power/Service/Dockerfile new file mode 100644 index 0000000..c068411 --- /dev/null +++ b/Power/Service/Dockerfile @@ -0,0 +1,16 @@ +FROM mcr.microsoft.com/dotnet/core/aspnet:3.0.0-preview7-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/core/sdk:3.0.100-preview7-buster AS build +WORKDIR /src +COPY ["./Service.csproj", "./"] +RUN dotnet restore "Service.csproj" +COPY . . +WORKDIR "/src" +RUN dotnet publish "Service.csproj" -c Release -o /app + +FROM base AS final +WORKDIR /app +COPY --from=build /app . +ENTRYPOINT ["dotnet", "ChrisKaczor.HomeMonitor.Power.Service.dll"] \ No newline at end of file diff --git a/Power/Service/Models/PowerChannel.cs b/Power/Service/Models/PowerChannel.cs new file mode 100644 index 0000000..cf57f68 --- /dev/null +++ b/Power/Service/Models/PowerChannel.cs @@ -0,0 +1,30 @@ +using JetBrains.Annotations; +using System.Text.Json.Serialization; + +namespace ChrisKaczor.HomeMonitor.Power.Service.Models +{ + [UsedImplicitly] + public class PowerChannel + { + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("ch")] + public long ChannelNumber { get; set; } + + [JsonPropertyName("eImp_Ws")] + public long ImportedEnergy { get; set; } + + [JsonPropertyName("eExp_Ws")] + public long ExportedEnergy { get; set; } + + [JsonPropertyName("p_W")] + public long RealPower { get; set; } + + [JsonPropertyName("q_VAR")] + public long ReactivePower { get; set; } + + [JsonPropertyName("v_V")] + public double Voltage { get; set; } + } +} diff --git a/Power/Service/Models/PowerSample.cs b/Power/Service/Models/PowerSample.cs new file mode 100644 index 0000000..c7e19b3 --- /dev/null +++ b/Power/Service/Models/PowerSample.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace ChrisKaczor.HomeMonitor.Power.Service.Models +{ + public class PowerSample + { + [JsonPropertyName("sensorId")] + public string SensorId { get; set; } + + [JsonPropertyName("timestamp")] + public DateTimeOffset Timestamp { get; set; } + + [JsonPropertyName("channels")] + public PowerChannel[] Channels { get; set; } + + [JsonPropertyName("cts")] + public Dictionary[] CurrentTransformers { get; set; } + } +} diff --git a/Power/Service/PowerReader.cs b/Power/Service/PowerReader.cs new file mode 100644 index 0000000..d63f86a --- /dev/null +++ b/Power/Service/PowerReader.cs @@ -0,0 +1,81 @@ +using ChrisKaczor.HomeMonitor.Power.Service.Models; +using JetBrains.Annotations; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using RestSharp; +using System; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace ChrisKaczor.HomeMonitor.Power.Service +{ + [UsedImplicitly] + public class PowerReader : IHostedService + { + private readonly IConfiguration _configuration; + + private HubConnection _hubConnection; + private Timer _readTimer; + + public PowerReader(IConfiguration configuration) + { + _configuration = configuration; + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _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) + { + 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(response.Content); + + Console.WriteLine(sample.Channels[2].Type + " " + sample.Channels[2].RealPower); + Console.WriteLine(sample.Channels[3].Type + " " + sample.Channels[3].RealPower); + + if (_hubConnection == null) + return; + + try + { + if (_hubConnection.State == HubConnectionState.Disconnected) + _hubConnection.StartAsync().Wait(); + + _hubConnection.InvokeAsync("SendLatestSample", response.Content).Wait(); + } + catch (Exception exception) + { + WriteLog($"Hub exception: {exception}"); + } + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _readTimer.Dispose(); + + _hubConnection?.StopAsync(cancellationToken).Wait(cancellationToken); + + return Task.CompletedTask; + } + + private static void WriteLog(string message) + { + Console.WriteLine(message); + } + } +} diff --git a/Power/Service/Program.cs b/Power/Service/Program.cs new file mode 100644 index 0000000..ec72b77 --- /dev/null +++ b/Power/Service/Program.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; + +namespace ChrisKaczor.HomeMonitor.Power.Service +{ + public static class Program + { + public static void Main(string[] args) + { + CreateWebHostBuilder(args).Build().Run(); + } + + private static IWebHostBuilder CreateWebHostBuilder(string[] args) + { + return WebHost.CreateDefaultBuilder(args).ConfigureAppConfiguration((hostingContext, config) => + { + config.AddEnvironmentVariables(); + }).UseStartup(); + } + } +} \ No newline at end of file diff --git a/Power/Service/Properties/launchSettings.json b/Power/Service/Properties/launchSettings.json new file mode 100644 index 0000000..cbedba3 --- /dev/null +++ b/Power/Service/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "Service": { + "commandName": "Project", + "launchUrl": "values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5001" + } + } +} \ No newline at end of file diff --git a/Power/Service/Service.csproj b/Power/Service/Service.csproj new file mode 100644 index 0000000..189d5ce --- /dev/null +++ b/Power/Service/Service.csproj @@ -0,0 +1,17 @@ + + + + true + netcoreapp3.0 + InProcess + ChrisKaczor.HomeMonitor.Power.Service + ChrisKaczor.HomeMonitor.Power.Service + + + + + + + + + diff --git a/Power/Service/Service.csproj.user b/Power/Service/Service.csproj.user new file mode 100644 index 0000000..f0bf193 --- /dev/null +++ b/Power/Service/Service.csproj.user @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + Service + + \ No newline at end of file diff --git a/Power/Service/Startup.cs b/Power/Service/Startup.cs new file mode 100644 index 0000000..70c35e2 --- /dev/null +++ b/Power/Service/Startup.cs @@ -0,0 +1,50 @@ +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; + +namespace ChrisKaczor.HomeMonitor.Power.Service +{ + public class Startup + { + public void ConfigureServices(IServiceCollection services) + { + 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().SetCompatibilityVersion(CompatibilityVersion.Version_3_0); + } + + public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment environment) + { + if (environment.IsDevelopment()) + applicationBuilder.UseDeveloperExceptionPage(); + + applicationBuilder.UseCors("CorsPolicy"); + + applicationBuilder.UseResponseCompression(); + + applicationBuilder.UseRouting(); + + applicationBuilder.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + } + } +} \ No newline at end of file diff --git a/Power/Service/appsettings.json b/Power/Service/appsettings.json new file mode 100644 index 0000000..373c2f1 --- /dev/null +++ b/Power/Service/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "Hub": { + "Power": "http://hub-server/power" + } +} \ No newline at end of file diff --git a/Power/Service/deploy/azure-pipelines.yml b/Power/Service/deploy/azure-pipelines.yml new file mode 100644 index 0000000..49c6d52 --- /dev/null +++ b/Power/Service/deploy/azure-pipelines.yml @@ -0,0 +1,43 @@ +name: $(Rev:r) + +trigger: + batch: 'true' + branches: + include: + - master + paths: + include: + - Power/Service + +pool: + name: Hosted Ubuntu 1604 + +steps: +- task: Docker@0 + displayName: 'Build an image' + inputs: + containerregistrytype: 'Container Registry' + dockerRegistryConnection: 'Docker Hub' + dockerFile: 'Power/Service/Dockerfile' + imageName: 'ckaczor/home-monitor-power-service:$(Build.BuildNumber)' + includeLatestTag: true + +- task: Docker@0 + displayName: 'Push an image' + inputs: + containerregistrytype: 'Container Registry' + dockerRegistryConnection: 'Docker Hub' + action: 'Push an image' + imageName: 'ckaczor/home-monitor-power-service:$(Build.BuildNumber)' + includeLatestTag: true + +- task: Bash@3 + inputs: + targetType: 'inline' + script: 'sed -i s/#BUILD_BUILDNUMBER#/$BUILD_BUILDNUMBER/ Power/Service/deploy/manifest.yaml' + +- task: PublishBuildArtifacts@1 + inputs: + PathtoPublish: 'Power/Service/deploy/manifest.yaml' + ArtifactName: 'Manifest' + publishLocation: 'Container' \ No newline at end of file diff --git a/Power/Service/deploy/manifest.yaml b/Power/Service/deploy/manifest.yaml new file mode 100644 index 0000000..2593133 --- /dev/null +++ b/Power/Service/deploy/manifest.yaml @@ -0,0 +1,144 @@ +--- +kind: StatefulSet +apiVersion: apps/v1 +metadata: + name: power-database + namespace: home-monitor + labels: + app: power-database +spec: + replicas: 1 + selector: + matchLabels: + app: power-database + serviceName: power-database + template: + metadata: + labels: + app: power-database + spec: + containers: + - name: power-database + image: mcr.microsoft.com/mssql/server + terminationMessagePath: "/dev/termination-log" + terminationMessagePolicy: File + imagePullPolicy: IfNotPresent + env: + - name: SA_PASSWORD + valueFrom: + secretKeyRef: + name: power-database-credentials + key: password + - name: ACCEPT_EULA + value: "Y" + - name: MSSQL_PID + value: Express + - name: TZ + value: America/New_York + volumeMounts: + - name: data + mountPath: /var/opt/mssql + restartPolicy: Always + terminationGracePeriodSeconds: 30 + dnsPolicy: ClusterFirst + nodeSelector: + kubernetes.io/hostname: kubernetes + schedulerName: default-scheduler + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: [ "ReadWriteOnce" ] + storageClassName: local-path + resources: + requests: + storage: 4Gi +--- +kind: Service +apiVersion: v1 +metadata: + name: power-database +spec: + ports: + - name: client + port: 1433 + selector: + app: power-database + type: LoadBalancer +--- +kind: Deployment +apiVersion: apps/v1 +metadata: + name: power-service + namespace: home-monitor + labels: + app: power-service +spec: + replicas: 1 + selector: + matchLabels: + app: power-service + template: + metadata: + labels: + app: power-service + spec: + containers: + - name: power-service + image: ckaczor/home-monitor-power-service:#BUILD_BUILDNUMBER# + terminationMessagePath: "/dev/termination-log" + terminationMessagePolicy: File + imagePullPolicy: Always + securityContext: + privileged: true + env: + - name: Power__Database__Host + value: power-database + - name: Power__Database__User + valueFrom: + secretKeyRef: + name: power-database-credentials + key: username + - name: Power__Database__Password + valueFrom: + secretKeyRef: + name: power-database-credentials + key: password + - name: Hub__Power + value: http://hub-service/power + restartPolicy: Always + terminationGracePeriodSeconds: 30 + dnsPolicy: ClusterFirst + nodeSelector: + kubernetes.io/hostname: kubernetes + schedulerName: default-scheduler +--- +kind: Service +apiVersion: v1 +metadata: + name: power-service +spec: + ports: + - name: client + port: 80 + selector: + app: power-service + type: ClusterIP +--- +kind: Ingress +apiVersion: extensions/v1beta1 +metadata: + name: power + namespace: home-monitor + annotations: + kubernetes.io/ingress.class: traefik + nginx.ingress.kubernetes.io/ssl-redirect: 'false' + traefik.frontend.rule.type: PathPrefixStrip +spec: + rules: + - http: + paths: + - path: "/api/power" + backend: + serviceName: power-service + servicePort: 80 \ No newline at end of file