diff --git a/DeviceStatus/Service/Service.sln b/DeviceStatus/Service/Service.sln
new file mode 100644
index 0000000..28595d2
--- /dev/null
+++ b/DeviceStatus/Service/Service.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "Service.csproj", "{AC509ACF-B729-408E-B9A1-83EE811A2FA0}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AC509ACF-B729-408E-B9A1-83EE811A2FA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AC509ACF-B729-408E-B9A1-83EE811A2FA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AC509ACF-B729-408E-B9A1-83EE811A2FA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AC509ACF-B729-408E-B9A1-83EE811A2FA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {A50962D3-359A-49D1-ADEB-8D49FAECF950}
+ EndGlobalSection
+EndGlobal
diff --git a/Environment/Environment.sln b/Environment/Environment.sln
new file mode 100644
index 0000000..8736251
--- /dev/null
+++ b/Environment/Environment.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.8.34330.188
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "Service\Service.csproj", "{AAF49637-5EAB-4D0F-A3EB-4421456DF709}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {AAF49637-5EAB-4D0F-A3EB-4421456DF709}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {AAF49637-5EAB-4D0F-A3EB-4421456DF709}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {AAF49637-5EAB-4D0F-A3EB-4421456DF709}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {AAF49637-5EAB-4D0F-A3EB-4421456DF709}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {AF8B4FE8-1A8C-4189-8A16-BFD216D13047}
+ EndGlobalSection
+EndGlobal
diff --git a/Environment/Environment.sln.DotSettings b/Environment/Environment.sln.DotSettings
new file mode 100644
index 0000000..86c4878
--- /dev/null
+++ b/Environment/Environment.sln.DotSettings
@@ -0,0 +1,195 @@
+
+ <?xml version="1.0" encoding="utf-16"?>
+<Patterns xmlns="urn:schemas-jetbrains-com:member-reordering-patterns">
+ <TypePattern DisplayName="Non-reorderable types">
+ <TypePattern.Match>
+ <Or>
+ <And>
+ <Kind Is="Interface" />
+ <Or>
+ <HasAttribute Name="System.Runtime.InteropServices.InterfaceTypeAttribute" />
+ <HasAttribute Name="System.Runtime.InteropServices.ComImport" />
+ </Or>
+ </And>
+ <Kind Is="Struct" />
+ <HasAttribute Name="JetBrains.Annotations.NoReorderAttribute" />
+ <HasAttribute Name="JetBrains.Annotations.NoReorder" />
+ </Or>
+ </TypePattern.Match>
+ </TypePattern>
+ <TypePattern DisplayName="xUnit.net Test Classes" RemoveRegions="All">
+ <TypePattern.Match>
+ <And>
+ <Kind Is="Class" />
+ <HasMember>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="Xunit.FactAttribute" Inherited="True" />
+ <HasAttribute Name="Xunit.TheoryAttribute" Inherited="True" />
+ </And>
+ </HasMember>
+ </And>
+ </TypePattern.Match>
+ <Entry DisplayName="Setup/Teardown Methods">
+ <Entry.Match>
+ <Or>
+ <Kind Is="Constructor" />
+ <And>
+ <Kind Is="Method" />
+ <ImplementsInterface Name="System.IDisposable" />
+ </And>
+ </Or>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Kind Order="Constructor" />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="All other members" />
+ <Entry DisplayName="Test Methods" Priority="100">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="Xunit.FactAttribute" />
+ <HasAttribute Name="Xunit.TheoryAttribute" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ </TypePattern>
+ <TypePattern DisplayName="NUnit Test Fixtures" RemoveRegions="All">
+ <TypePattern.Match>
+ <And>
+ <Kind Is="Class" />
+ <Or>
+ <HasAttribute Name="NUnit.Framework.TestFixtureAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureSourceAttribute" Inherited="True" />
+ <HasMember>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="NUnit.Framework.TestAttribute" />
+ <HasAttribute Name="NUnit.Framework.TestCaseAttribute" />
+ <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" />
+ </And>
+ </HasMember>
+ </Or>
+ </And>
+ </TypePattern.Match>
+ <Entry DisplayName="Setup/Teardown Methods">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <Or>
+ <HasAttribute Name="NUnit.Framework.SetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.TearDownAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureSetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.TestFixtureTearDownAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.OneTimeSetUpAttribute" Inherited="True" />
+ <HasAttribute Name="NUnit.Framework.OneTimeTearDownAttribute" Inherited="True" />
+ </Or>
+ </And>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="All other members" />
+ <Entry DisplayName="Test Methods" Priority="100">
+ <Entry.Match>
+ <And>
+ <Kind Is="Method" />
+ <HasAttribute Name="NUnit.Framework.TestAttribute" />
+ <HasAttribute Name="NUnit.Framework.TestCaseAttribute" />
+ <HasAttribute Name="NUnit.Framework.TestCaseSourceAttribute" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ </TypePattern>
+ <TypePattern DisplayName="Default Pattern">
+ <Entry DisplayName="Public Delegates" Priority="100">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Delegate" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Public Enums" Priority="100">
+ <Entry.Match>
+ <And>
+ <Access Is="Public" />
+ <Kind Is="Enum" />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Static Fields and Constants">
+ <Entry.Match>
+ <Or>
+ <Kind Is="Constant" />
+ <And>
+ <Kind Is="Field" />
+ <Static />
+ </And>
+ </Or>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Kind Order="Constant Field" />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Fields">
+ <Entry.Match>
+ <And>
+ <Kind Is="Field" />
+ <Not>
+ <Static />
+ </Not>
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <Readonly />
+ <Name />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Constructors">
+ <Entry.Match>
+ <Kind Is="Constructor" />
+ </Entry.Match>
+ <Entry.SortBy>
+ <Static />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="Properties, Indexers">
+ <Entry.Match>
+ <Or>
+ <Kind Is="Property" />
+ <Kind Is="Indexer" />
+ </Or>
+ </Entry.Match>
+ </Entry>
+ <Entry DisplayName="Interface Implementations" Priority="100">
+ <Entry.Match>
+ <And>
+ <Kind Is="Member" />
+ <ImplementsInterface />
+ </And>
+ </Entry.Match>
+ <Entry.SortBy>
+ <ImplementsInterface Immediate="True" />
+ </Entry.SortBy>
+ </Entry>
+ <Entry DisplayName="All other members" />
+ <Entry DisplayName="Nested Types">
+ <Entry.Match>
+ <Kind Is="Type" />
+ </Entry.Match>
+ </Entry>
+ </TypePattern>
+</Patterns>
+ True
\ No newline at end of file
diff --git a/Environment/Service/Data/Database.cs b/Environment/Service/Data/Database.cs
new file mode 100644
index 0000000..75b8e59
--- /dev/null
+++ b/Environment/Service/Data/Database.cs
@@ -0,0 +1,51 @@
+using System.Reflection;
+using Dapper;
+using DbUp;
+using Microsoft.Data.SqlClient;
+
+namespace ChrisKaczor.HomeMonitor.Environment.Service.Data;
+
+public class Database(IConfiguration configuration)
+{
+ private string GetConnectionString()
+ {
+ var connectionStringBuilder = new SqlConnectionStringBuilder
+ {
+ DataSource = configuration["Environment:Database:Host"],
+ UserID = configuration["Environment:Database:User"],
+ Password = configuration["Environment:Database:Password"],
+ InitialCatalog = configuration["Environment:Database:Name"],
+ TrustServerCertificate = bool.Parse(configuration["Environment:Database:TrustServerCertificate"] ?? "false")
+ };
+
+ return connectionStringBuilder.ConnectionString;
+ }
+
+ public void EnsureDatabase()
+ {
+ var connectionString = GetConnectionString();
+
+ DbUp.EnsureDatabase.For.SqlDatabase(connectionString);
+
+ var upgradeEngine = DeployChanges.To.SqlDatabase(connectionString).WithScriptsEmbeddedInAssembly(Assembly.GetExecutingAssembly(), s => s.Contains(".Schema.")).LogToConsole().Build();
+
+ upgradeEngine.PerformUpgrade();
+ }
+
+ private SqlConnection CreateConnection()
+ {
+ var connection = new SqlConnection(GetConnectionString());
+ connection.Open();
+
+ return connection;
+ }
+
+ public async Task StoreMessageAsync(Message message)
+ {
+ await using var connection = CreateConnection();
+
+ var query = ResourceReader.GetString("ChrisKaczor.HomeMonitor.Environment.Service.Data.Queries.CreateReading.sql");
+
+ await connection.QueryAsync(query, message);
+ }
+}
\ No newline at end of file
diff --git a/Environment/Service/Data/Queries/CreateReading.sql b/Environment/Service/Data/Queries/CreateReading.sql
new file mode 100644
index 0000000..06ecd86
--- /dev/null
+++ b/Environment/Service/Data/Queries/CreateReading.sql
@@ -0,0 +1,25 @@
+BEGIN TRANSACTION
+
+INSERT Reading
+ (Timestamp, Name, Model, Temperature, Pressure, Humidity, Luminance, GasResistance, ColorTemperature, AirQualityIndex)
+SELECT
+ @Timestamp,
+ @Name,
+ @Model,
+ @Temperature,
+ @Pressure,
+ @Humidity,
+ @Luminance,
+ @GasResistance,
+ @ColorTemperature,
+ @AirQualityIndex
+WHERE NOT EXISTS
+ (
+ SELECT
+ 1
+ FROM
+ Reading WITH (UPDLOCK, SERIALIZABLE)
+ WHERE Timestamp = @Timestamp AND Name = @Name AND Model = @Model
+ )
+
+COMMIT TRANSACTION
\ No newline at end of file
diff --git a/Environment/Service/Data/Schema/1-Initial Schema.sql b/Environment/Service/Data/Schema/1-Initial Schema.sql
new file mode 100644
index 0000000..a24e750
--- /dev/null
+++ b/Environment/Service/Data/Schema/1-Initial Schema.sql
@@ -0,0 +1,14 @@
+CREATE TABLE Reading
+(
+ Timestamp datetimeoffset NOT NULL,
+ Name nvarchar(50) NOT NULL,
+ Model nvarchar(50) NOT NULL,
+ Temperature decimal(5, 2) NOT NULL,
+ Pressure decimal(6, 2) NOT NULL,
+ Humidity decimal(5, 2) NOT NULL,
+ Luminance int NOT NULL,
+ GasResistance int NOT NULL,
+ ColorTemperature int NOT NULL,
+ AirQualityIndex decimal(4, 1) NOT NULL,
+ CONSTRAINT reading_pk PRIMARY KEY (Timestamp, Name, Model)
+);
diff --git a/Environment/Service/Dockerfile b/Environment/Service/Dockerfile
new file mode 100644
index 0000000..e7c8478
--- /dev/null
+++ b/Environment/Service/Dockerfile
@@ -0,0 +1,17 @@
+FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS base
+WORKDIR /app
+EXPOSE 80
+
+FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine 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
+RUN apk add --no-cache tzdata
+WORKDIR /app
+COPY --from=build /app .
+ENTRYPOINT ["dotnet", "ChrisKaczor.HomeMonitor.Environment.Service.dll"]
\ No newline at end of file
diff --git a/Environment/Service/Environment.http b/Environment/Service/Environment.http
new file mode 100644
index 0000000..f257474
--- /dev/null
+++ b/Environment/Service/Environment.http
@@ -0,0 +1,6 @@
+@Environment_HostAddress = http://localhost:5234
+
+GET {{Environment_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/Environment/Service/Message.cs b/Environment/Service/Message.cs
new file mode 100644
index 0000000..d1cfb12
--- /dev/null
+++ b/Environment/Service/Message.cs
@@ -0,0 +1,32 @@
+using System.Text.Json.Serialization;
+
+namespace ChrisKaczor.HomeMonitor.Environment.Service;
+
+public class Message
+{
+ [JsonPropertyName("model")]
+ public required string Model { get; set; }
+
+ [JsonPropertyName("nickname")]
+ public required string Name { get; set; }
+
+ [JsonPropertyName("readings")]
+ public required Readings Readings { get; set; }
+
+ [JsonPropertyName("timestamp")]
+ public required DateTimeOffset Timestamp { get; set; }
+
+ public decimal AirQualityIndex => Readings.AirQualityIndex;
+
+ public decimal ColorTemperature => Readings.ColorTemperature;
+
+ public decimal GasResistance => Readings.GasResistance;
+
+ public decimal Humidity => Readings.Humidity;
+
+ public decimal Luminance => Readings.Luminance;
+
+ public decimal Pressure => Readings.Pressure;
+
+ public decimal Temperature => Readings.Temperature;
+}
\ No newline at end of file
diff --git a/Environment/Service/MessageHandler.cs b/Environment/Service/MessageHandler.cs
new file mode 100644
index 0000000..61dabcc
--- /dev/null
+++ b/Environment/Service/MessageHandler.cs
@@ -0,0 +1,68 @@
+using System.Text.Json;
+using ChrisKaczor.HomeMonitor.Environment.Service.Data;
+using MQTTnet;
+using MQTTnet.Client;
+
+namespace ChrisKaczor.HomeMonitor.Environment.Service;
+
+public class MessageHandler : IHostedService
+{
+ private readonly IConfiguration _configuration;
+ private readonly Database _database;
+ private readonly IMqttClient _mqttClient;
+
+ private readonly MqttFactory _mqttFactory;
+ private readonly string _topic;
+
+ public MessageHandler(IConfiguration configuration, Database database)
+ {
+ _configuration = configuration;
+ _database = database;
+
+ _database.EnsureDatabase();
+
+ _topic = _configuration["Mqtt:Topic"] ?? string.Empty;
+
+ if (string.IsNullOrEmpty(_topic))
+ throw new InvalidOperationException("Topic not set");
+
+ _mqttFactory = new MqttFactory();
+ _mqttClient = _mqttFactory.CreateMqttClient();
+
+ _mqttClient.ApplicationMessageReceivedAsync += OnApplicationMessageReceivedAsync;
+ }
+
+ public async Task StartAsync(CancellationToken cancellationToken)
+ {
+ var mqttClientOptions = new MqttClientOptionsBuilder().WithTcpServer(_configuration["Mqtt:Server"]).Build();
+ await _mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None);
+
+ var mqttSubscribeOptions = _mqttFactory.CreateSubscribeOptionsBuilder().WithTopicFilter($"{_topic}/#").Build();
+ await _mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None);
+ }
+
+ public async Task StopAsync(CancellationToken cancellationToken)
+ {
+ await _mqttClient.DisconnectAsync(new MqttClientDisconnectOptionsBuilder().Build(), CancellationToken.None);
+ }
+
+ private async Task OnApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg)
+ {
+ var topic = arg.ApplicationMessage.Topic;
+ var payload = arg.ApplicationMessage.ConvertPayloadToString();
+
+ WriteLog($"Topic: {topic} = {payload}");
+
+ var message = JsonSerializer.Deserialize(payload);
+
+ if (message == null)
+ return;
+
+ await _database.StoreMessageAsync(message);
+ }
+
+ private static void WriteLog(string message)
+ {
+ Console.WriteLine(message);
+ }
+}
\ No newline at end of file
diff --git a/Environment/Service/Program.cs b/Environment/Service/Program.cs
new file mode 100644
index 0000000..47788ad
--- /dev/null
+++ b/Environment/Service/Program.cs
@@ -0,0 +1,26 @@
+using ChrisKaczor.HomeMonitor.Environment.Service.Data;
+
+namespace ChrisKaczor.HomeMonitor.Environment.Service;
+
+public static class Program
+{
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.Configuration.AddEnvironmentVariables();
+
+ builder.Services.AddControllers();
+
+ builder.Services.AddTransient();
+ builder.Services.AddHostedService();
+
+ var app = builder.Build();
+
+ app.UseAuthorization();
+
+ app.MapControllers();
+
+ app.Run();
+ }
+}
\ No newline at end of file
diff --git a/Environment/Service/Properties/launchSettings.json b/Environment/Service/Properties/launchSettings.json
new file mode 100644
index 0000000..2ca9931
--- /dev/null
+++ b/Environment/Service/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "Environment": {
+ "commandName": "Project",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Environment/Service/Readings.cs b/Environment/Service/Readings.cs
new file mode 100644
index 0000000..c1f4919
--- /dev/null
+++ b/Environment/Service/Readings.cs
@@ -0,0 +1,29 @@
+using System.Text.Json.Serialization;
+using JetBrains.Annotations;
+
+namespace ChrisKaczor.HomeMonitor.Environment.Service;
+
+[UsedImplicitly]
+public class Readings
+{
+ [JsonPropertyName("aqi")]
+ public decimal AirQualityIndex { get; set; }
+
+ [JsonPropertyName("color_temperature")]
+ public decimal ColorTemperature { get; set; }
+
+ [JsonPropertyName("gas_resistance")]
+ public decimal GasResistance { get; set; }
+
+ [JsonPropertyName("humidity")]
+ public decimal Humidity { get; set; }
+
+ [JsonPropertyName("luminance")]
+ public decimal Luminance { get; set; }
+
+ [JsonPropertyName("pressure")]
+ public decimal Pressure { get; set; }
+
+ [JsonPropertyName("temperature")]
+ public decimal Temperature { get; set; }
+}
\ No newline at end of file
diff --git a/Environment/Service/ResourceReader.cs b/Environment/Service/ResourceReader.cs
new file mode 100644
index 0000000..f62f576
--- /dev/null
+++ b/Environment/Service/ResourceReader.cs
@@ -0,0 +1,19 @@
+using System.Reflection;
+
+namespace ChrisKaczor.HomeMonitor.Environment.Service;
+
+public static class ResourceReader
+{
+ public static string GetString(string resourceName)
+ {
+ var assembly = Assembly.GetExecutingAssembly();
+
+ 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())}.");
+
+ using var reader = new StreamReader(stream);
+ return reader.ReadToEnd();
+ }
+}
\ No newline at end of file
diff --git a/Environment/Service/Service.csproj b/Environment/Service/Service.csproj
new file mode 100644
index 0000000..821e2cf
--- /dev/null
+++ b/Environment/Service/Service.csproj
@@ -0,0 +1,33 @@
+
+
+
+ net8.0
+ enable
+ enable
+ false
+ ChrisKaczor.HomeMonitor.Environment.Service
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Environment/Service/appsettings.Development.json b/Environment/Service/appsettings.Development.json
new file mode 100644
index 0000000..196c711
--- /dev/null
+++ b/Environment/Service/appsettings.Development.json
@@ -0,0 +1,20 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "Mqtt": {
+ "Server": "172.23.10.3"
+ },
+ "Environment": {
+ "Database": {
+ "Host": "localhost",
+ "User": "sa",
+ "Password": "newpassword",
+ "Name": "Environment",
+ "TrustServerCertificate": true
+ }
+ }
+}
diff --git a/Environment/Service/appsettings.json b/Environment/Service/appsettings.json
new file mode 100644
index 0000000..5566089
--- /dev/null
+++ b/Environment/Service/appsettings.json
@@ -0,0 +1,22 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*",
+ "Mqtt": {
+ "Server": "mosquitto",
+ "Topic": "enviro"
+ },
+ "Environment": {
+ "Database": {
+ "Host": "",
+ "User": "",
+ "Password": "",
+ "Name": "Environment",
+ "TrustServerCertificate": false
+ }
+ }
+}
diff --git a/Environment/Service/deploy/azure-pipelines.yml b/Environment/Service/deploy/azure-pipelines.yml
new file mode 100644
index 0000000..aab7031
--- /dev/null
+++ b/Environment/Service/deploy/azure-pipelines.yml
@@ -0,0 +1,72 @@
+name: $(Rev:r)
+
+pr: none
+
+trigger:
+ batch: 'true'
+ branches:
+ include:
+ - master
+ paths:
+ include:
+ - Environment/Service
+
+stages:
+- stage: Build
+ jobs:
+ - job: Build
+ pool:
+ vmImage: 'ubuntu-latest'
+
+ steps:
+ - task: Docker@0
+ displayName: 'Build an image'
+ inputs:
+ containerregistrytype: 'Container Registry'
+ dockerRegistryConnection: 'Docker Hub'
+ dockerFile: 'Environment/Service/Dockerfile'
+ imageName: 'ckaczor/home-monitor-environment-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-environment-service:$(Build.BuildNumber)'
+ includeLatestTag: true
+
+ - task: Bash@3
+ inputs:
+ targetType: 'inline'
+ script: 'sed -i s/#BUILD_BUILDNUMBER#/$BUILD_BUILDNUMBER/ Environment/Service/deploy/manifest.yaml'
+
+ - task: PublishBuildArtifacts@1
+ inputs:
+ PathtoPublish: 'Environment/Service/deploy/manifest.yaml'
+ ArtifactName: 'Manifest'
+ publishLocation: 'Container'
+
+- stage: Deploy
+ jobs:
+ - job: Deploy
+ pool:
+ vmImage: 'ubuntu-latest'
+ steps:
+ - task: DownloadBuildArtifacts@0
+ inputs:
+ artifactName: 'Manifest'
+ buildType: 'current'
+ downloadType: 'single'
+ downloadPath: '$(System.ArtifactsDirectory)'
+ - task: Kubernetes@1
+ inputs:
+ connectionType: 'Kubernetes Service Connection'
+ kubernetesServiceEndpoint: 'Kubernetes'
+ namespace: 'home-monitor'
+ command: 'apply'
+ useConfigurationFile: true
+ configuration: '$(System.ArtifactsDirectory)/Manifest/manifest.yaml'
+ secretType: 'dockerRegistry'
+ containerRegistryType: 'Container Registry'
\ No newline at end of file
diff --git a/Environment/Service/deploy/manifest.yaml b/Environment/Service/deploy/manifest.yaml
new file mode 100644
index 0000000..8799fe9
--- /dev/null
+++ b/Environment/Service/deploy/manifest.yaml
@@ -0,0 +1,158 @@
+---
+kind: StatefulSet
+apiVersion: apps/v1
+metadata:
+ name: environment-database
+ namespace: home-monitor
+ labels:
+ app: environment-database
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: environment-database
+ serviceName: environment-database
+ template:
+ metadata:
+ labels:
+ app: environment-database
+ spec:
+ containers:
+ - name: environment-database
+ image: mcr.microsoft.com/mssql/server
+ terminationMessagePath: "/dev/termination-log"
+ terminationMessagePolicy: File
+ imagePullPolicy: IfNotPresent
+ env:
+ - name: SA_PASSWORD
+ valueFrom:
+ secretKeyRef:
+ name: environment-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: environment-database
+spec:
+ ports:
+ - name: client
+ port: 1433
+ selector:
+ app: environment-database
+ type: LoadBalancer
+---
+kind: Deployment
+apiVersion: apps/v1
+metadata:
+ name: environment-service
+ namespace: home-monitor
+ labels:
+ app: environment-service
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: environment-service
+ template:
+ metadata:
+ labels:
+ app: environment-service
+ spec:
+ containers:
+ - name: environment-service
+ image: ckaczor/home-monitor-environment-service:#BUILD_BUILDNUMBER#
+ terminationMessagePath: "/dev/termination-log"
+ terminationMessagePolicy: File
+ imagePullPolicy: Always
+ securityContext:
+ privileged: true
+ env:
+ - name: Environment__Database__Host
+ value: environment-database
+ - name: Environment__Database__User
+ valueFrom:
+ secretKeyRef:
+ name: environment-database-credentials
+ key: username
+ - name: Environment__Database__Password
+ valueFrom:
+ secretKeyRef:
+ name: environment-database-credentials
+ key: password
+ - name: Hub__Environment
+ value: http://hub-service/environment
+ restartPolicy: Always
+ terminationGracePeriodSeconds: 30
+ dnsPolicy: ClusterFirst
+ nodeSelector:
+ kubernetes.io/hostname: kubernetes
+ schedulerName: default-scheduler
+---
+kind: Service
+apiVersion: v1
+metadata:
+ name: environment-service
+spec:
+ ports:
+ - name: client
+ port: 80
+ selector:
+ app: environment-service
+ type: ClusterIP
+---
+apiVersion: traefik.containo.us/v1alpha1
+kind: IngressRoute
+metadata:
+ annotations:
+ kubernetes.io/ingress.class: traefik
+ creationTimestamp: null
+ name: environment
+ namespace: home-monitor
+spec:
+ routes:
+ - kind: Rule
+ match: PathPrefix(`/api/environment`)
+ middlewares:
+ - name: api-environment
+ namespace: home-monitor
+ services:
+ - kind: Service
+ name: environment-service
+ namespace: home-monitor
+ port: 80
+---
+apiVersion: traefik.containo.us/v1alpha1
+kind: Middleware
+metadata:
+ creationTimestamp: null
+ name: api-environment
+ namespace: home-monitor
+spec:
+ stripPrefix:
+ prefixes:
+ - /api/environment
diff --git a/Hub/Service/Hubs/EnvironmentHub.cs b/Hub/Service/Hubs/EnvironmentHub.cs
new file mode 100644
index 0000000..3bc108c
--- /dev/null
+++ b/Hub/Service/Hubs/EnvironmentHub.cs
@@ -0,0 +1,19 @@
+using JetBrains.Annotations;
+using Microsoft.AspNetCore.SignalR;
+using System;
+using System.Threading.Tasks;
+
+namespace ChrisKaczor.HomeMonitor.Hub.Service.Hubs
+{
+ [UsedImplicitly]
+ public class EnvironmentHub : Microsoft.AspNetCore.SignalR.Hub
+ {
+ [UsedImplicitly]
+ public async Task SendLatestReading(string message)
+ {
+ Console.WriteLine(message);
+
+ await Clients.Others.SendAsync("LatestReading", message);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Hub/Service/Startup.cs b/Hub/Service/Startup.cs
index 8fe3dc5..8b69e99 100644
--- a/Hub/Service/Startup.cs
+++ b/Hub/Service/Startup.cs
@@ -34,6 +34,7 @@ namespace ChrisKaczor.HomeMonitor.Hub.Service
endpoints.MapHub("/weather");
endpoints.MapHub("/power");
endpoints.MapHub("/device-status");
+ endpoints.MapHub("/environment");
endpoints.MapDefaultControllerRoute();
});
}
diff --git a/Weather/Service/Service.sln b/Weather/Service/Service.sln
new file mode 100644
index 0000000..abcaf41
--- /dev/null
+++ b/Weather/Service/Service.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.002.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "Service.csproj", "{8F323902-4BFE-4A92-ADDC-534F3DF4B497}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {8F323902-4BFE-4A92-ADDC-534F3DF4B497}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8F323902-4BFE-4A92-ADDC-534F3DF4B497}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8F323902-4BFE-4A92-ADDC-534F3DF4B497}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8F323902-4BFE-4A92-ADDC-534F3DF4B497}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {07D80FFE-643C-44E1-9BD3-1F5FD92C0527}
+ EndGlobalSection
+EndGlobal