Initial commit from private

This commit is contained in:
2019-07-15 20:51:25 -04:00
commit 264f03a22f
55 changed files with 2006 additions and 0 deletions

9
.dockerignore Normal file
View File

@@ -0,0 +1,9 @@
.dockerignore
.env
.git
.gitignore
.vs
.vscode
*/bin
*/obj
**/.toolstarget

63
.gitattributes vendored Normal file
View File

@@ -0,0 +1,63 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
bin/
log/
obj/
.vs/
Private/

17
Dockerfile-HubService Normal file
View File

@@ -0,0 +1,17 @@
FROM microsoft/dotnet:2.2-aspnetcore-runtime-stretch-slim AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.2-sdk-stretch AS build
WORKDIR /src
COPY ["Hub/Service/Service.csproj", "Hub/Service/"]
COPY ["Weather/Models/Models.csproj", "Weather/Models/"]
RUN dotnet restore "Hub/Service/Service.csproj"
COPY . .
WORKDIR "/src/Hub/Service"
RUN dotnet publish "Service.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "Hub.Service.dll"]

View File

@@ -0,0 +1,16 @@
FROM mcr.microsoft.com/dotnet/core/aspnet:3.0.0-preview6-buster-slim-arm32v7 AS base
WORKDIR /app
FROM mcr.microsoft.com/dotnet/core/sdk:3.0.100-preview6-buster AS build
WORKDIR /src
COPY ["Weather/SerialReader/SerialReader.csproj", "Weather/SerialReader/"]
COPY ["Weather/Models/Models.csproj", "Weather/Models/"]
RUN dotnet restore -r linux-arm "Weather/SerialReader/SerialReader.csproj"
COPY . .
WORKDIR "/src/Weather/SerialReader"
RUN dotnet publish -r linux-arm --self-contained=false "SerialReader.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "Weather.SerialReader.dll"]

17
Dockerfile-WeatherService Normal file
View File

@@ -0,0 +1,17 @@
FROM microsoft/dotnet:2.2-aspnetcore-runtime-stretch-slim AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.2-sdk-stretch AS build
WORKDIR /src
COPY ["Weather/Service/Service.csproj", "Weather/Service/"]
COPY ["Weather/Models/Models.csproj", "Weather/Models/"]
RUN dotnet restore "Weather/Service/Service.csproj"
COPY . .
WORKDIR "/src/Weather/Service"
RUN dotnet publish "Service.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=build /app .
ENTRYPOINT ["dotnet", "Weather.Service.dll"]

31
Hub/Hub.sln Normal file
View File

@@ -0,0 +1,31 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28711.60
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Service", "Service\Service.csproj", "{11E9A9F4-9348-402E-8ADF-942F66965D32}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "..\Weather\Models\Models.csproj", "{7DE44178-B63E-4CC4-88AE-B322274EDE26}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{11E9A9F4-9348-402E-8ADF-942F66965D32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11E9A9F4-9348-402E-8ADF-942F66965D32}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11E9A9F4-9348-402E-8ADF-942F66965D32}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11E9A9F4-9348-402E-8ADF-942F66965D32}.Release|Any CPU.Build.0 = Release|Any CPU
{7DE44178-B63E-4CC4-88AE-B322274EDE26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7DE44178-B63E-4CC4-88AE-B322274EDE26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DE44178-B63E-4CC4-88AE-B322274EDE26}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DE44178-B63E-4CC4-88AE-B322274EDE26}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3A02B8D9-C20F-42A8-BEFD-F67E6BF56F16}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,3 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/AnalysisEnabled/@EntryValue">SOLUTION</s:String>
<s:Int64 x:Key="/Default/CodeStyle/Naming/CSharpAutoNaming/AutoNamingCompletedVersion/@EntryValue">2</s:Int64></wpf:ResourceDictionary>

33
Hub/Service/.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,33 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/Service.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"launchBrowser": {
"enabled": false
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
]
}

35
Hub/Service/.vscode/tasks.json vendored Normal file
View File

@@ -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"
}
]
}

View File

@@ -0,0 +1,42 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace Hub.Service.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// GET api/values
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new[] { "value1", "value2" };
}
// GET api/values/5
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
// POST api/values
[HttpPost]
public void Post([FromBody] string value)
{
}
// PUT api/values/5
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value)
{
}
// DELETE api/values/5
[HttpDelete("{id}")]
public void Delete(int id)
{
}
}
}

View File

@@ -0,0 +1,19 @@
using JetBrains.Annotations;
using Microsoft.AspNetCore.SignalR;
using System;
using System.Threading.Tasks;
namespace Hub.Service.Hubs
{
[UsedImplicitly]
public class WeatherHub : Microsoft.AspNetCore.SignalR.Hub
{
[UsedImplicitly]
public async Task SendLatestReading(string message)
{
Console.WriteLine(message);
await Clients.Others.SendAsync("LatestReading", message);
}
}
}

18
Hub/Service/Program.cs Normal file
View File

@@ -0,0 +1,18 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
namespace Hub.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).UseStartup<Startup>();
}
}
}

View File

@@ -0,0 +1,30 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:53872",
"sslPort": 0
}
},
"profiles": {
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Service": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": "api/values",
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<AssemblyName>Hub.Service</AssemblyName>
<RootNamespace>Hub.Service</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Weather\Models\Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup>
<ActiveDebugProfile>Service</ActiveDebugProfile>
</PropertyGroup>
</Project>

30
Hub/Service/Startup.cs Normal file
View File

@@ -0,0 +1,30 @@
using Hub.Service.Hubs;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
namespace Hub.Service
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
services.AddSignalR().AddJsonProtocol(options => { options.PayloadSerializerSettings.ContractResolver = new Newtonsoft.Json.Serialization.DefaultContractResolver(); });
}
public void Configure(IApplicationBuilder applicationBuilder, IHostingEnvironment environment)
{
if (environment.IsDevelopment())
{
applicationBuilder.UseDeveloperExceptionPage();
}
applicationBuilder.UseSignalR(routes => { routes.MapHub<WeatherHub>("/weatherHub"); });
applicationBuilder.UseMvc();
}
}
}

View File

@@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@@ -0,0 +1,44 @@
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: hub-service
namespace: home-monitor
labels:
app: hub-service
spec:
replicas: 1
selector:
matchLabels:
app: hub-service
template:
metadata:
labels:
app: hub-service
spec:
containers:
- name: hub-service
image: ckaczor/home-monitor-hub-service:latest
terminationMessagePath: "/dev/termination-log"
terminationMessagePolicy: File
imagePullPolicy: Always
securityContext:
privileged: true
restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst
nodeSelector:
kubernetes.io/hostname: kubernetes
schedulerName: default-scheduler
---
kind: Service
apiVersion: v1
metadata:
name: hub-service
spec:
ports:
- name: client
port: 80
selector:
app: hub-service
type: ClusterIP

317
Weather/Arduino/Weather.ino Normal file
View File

@@ -0,0 +1,317 @@
#include <Wire.h> // I2C needed for sensors
#include "SparkFunMPL3115A2.h" // Pressure sensor - Search "SparkFun MPL3115" and install from Library Manager
#include "SparkFun_Si7021_Breakout_Library.h" // Humidity sensor - Search "SparkFun Si7021" and install from Library Manager
#include <SoftwareSerial.h> // Needed for GPS
#include <TinyGPS++.h> // GPS parsing - Available from https://github.com/mikalhart/TinyGPSPlus
static const int RXPin = 5; // GPS is attached to pin 5 (RX into GPS)
static const int TXPin = 4; // GPS is attached to pin 4 (TX from GPS)
TinyGPSPlus gps; // GPS module
SoftwareSerial ss(RXPin, TXPin); // Software serial port for GPS
MPL3115A2 pressureSensor; // Instance of the pressure sensor
Weather humiditySensor; // Instance of the humidity sensor
// Digital I/O pins
const byte WSPEED = 3; // Wind speed switch
const byte RAIN = 2; // Rain switch
const byte STAT1 = 7; // Blue status light
const byte STAT2 = 8; // Green status light
const byte GPS_PWRCTL = 6; // Pulling this pin low puts GPS to sleep but maintains RTC and RAM
// Analog I/O pins
const byte REFERENCE_3V3 = A3; // 3.3V reference
const byte LIGHT = A1; // Light level
const byte BATT = A2; // Battery level
const byte WDIR = A0; // Wind direction
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Global Variables
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
long lastSecond; // The millis counter to see when a second rolls by
long lastWindCheck = 0; // Time of the last wind check
int winddir = 0; // Instantaneous wind direction [0-360]
float windspeedmph = 0; // Instantaneous wind speed [mph]
float humidity = 0; // Instantaneous humidity [%]
float tempH = 0; // Instantaneous temperature from humidity sensor [F]
float tempP = 0; // Instantaneous temperature from pressure sensor [F]
float pressure = 0; // Instantaneous pressure [pascals]
float batt_lvl = 0; // Battery level [Analog value from 0 to 1023]
float light_lvl = 0; // Light level [Analog value from 0 to 1023]
// volatiles are subject to modification by IRQs
volatile unsigned long raintime, rainlast, raininterval, rain;
volatile long lastWindIRQ = 0;
volatile byte windClicks = 0;
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//Interrupt routines (these are called by the hardware interrupts, not by the main code)
//-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
void rainIRQ()
// Count rain gauge bucket tips as they occur
// Activated by the magnet and reed switch in the rain gauge, attached to input D2
{
raintime = millis(); // grab current time
raininterval = raintime - rainlast; // calculate interval between this and last event
if (raininterval > 10) // ignore switch-bounce glitches less than 10mS after initial edge
{
rain += 0.011; //Each dump is 0.011" of water
rainlast = raintime; // set up for next event
}
}
void wspeedIRQ()
// Activated by the magnet in the anemometer (2 ticks per rotation), attached to input D3
{
if (millis() - lastWindIRQ > 10) // Ignore switch-bounce glitches less than 10ms (142MPH max reading) after the reed switch closes
{
lastWindIRQ = millis(); //Grab the current time
windClicks++; //There is 1.492MPH for each click per second.
}
}
void setup()
{
Serial.begin(9600);
Serial.println("Board starting");
ss.begin(9600); //Begin listening to GPS over software serial at 9600. This should be the default baud of the module.
pinMode(STAT1, OUTPUT); //Status LED Blue
pinMode(STAT2, OUTPUT); //Status LED Green
pinMode(GPS_PWRCTL, OUTPUT);
digitalWrite(GPS_PWRCTL, HIGH); //Pulling this pin low puts GPS to sleep but maintains RTC and RAM
pinMode(WSPEED, INPUT_PULLUP); // input from wind meters windspeed sensor
pinMode(RAIN, INPUT_PULLUP); // input from wind meters rain gauge sensor
pinMode(REFERENCE_3V3, INPUT);
pinMode(LIGHT, INPUT);
//Configure the pressure sensor
pressureSensor.begin(); // Get sensor online
pressureSensor.setModeBarometer(); // Measure pressure in Pascals from 20 to 110 kPa
pressureSensor.setOversampleRate(7); // Set Oversample to the recommended 128
pressureSensor.enableEventFlags(); // Enable all three pressure and temp event flags
//Configure the humidity sensor
humiditySensor.begin();
lastSecond = millis();
// attach external interrupt pins to IRQ functions
attachInterrupt(0, rainIRQ, FALLING);
attachInterrupt(1, wspeedIRQ, FALLING);
// turn on interrupts
interrupts();
Serial.println("Board ready");
}
void loop()
{
//Keep track of which minute it is
if (millis() - lastSecond >= 1000)
{
digitalWrite(STAT1, HIGH); //Blink stat LED
lastSecond += 1000;
//Go calc all the various sensors
calcWeather();
//Report all readings every second
printWeather();
digitalWrite(STAT1, LOW); //Turn off stat LED
}
smartdelay(800); //Wait 1 second, and gather GPS data
}
//While we delay for a given amount of time, gather GPS data
static void smartdelay(unsigned long ms)
{
unsigned long start = millis();
do
{
while (ss.available())
gps.encode(ss.read());
} while (millis() - start < ms);
}
//Calculates each of the variables that wunderground is expecting
void calcWeather()
{
//Calc the wind speed and direction every second for 120 second to get 2 minute average
float currentSpeed = get_wind_speed();
windspeedmph = currentSpeed; //update global variable for windspeed when using the printWeather() function
//float currentSpeed = random(5); //For testing
int currentDirection = get_wind_direction();
//Calc winddir
winddir = get_wind_direction();
//Calc windspeed
//windspeedmph = get_wind_speed(); //This is calculated in the main loop on line 196
//Calc humidity
humidity = humiditySensor.getRH();
tempH = humiditySensor.readTempF();
//Calc tempf from pressure sensor
tempP = pressureSensor.readTempF();
//Calc pressure
pressure = pressureSensor.readPressure();
//Calc light level
light_lvl = get_light_level();
//Calc battery level
batt_lvl = get_battery_level();
}
//Returns the voltage of the light sensor based on the 3.3V rail
//This allows us to ignore what VCC might be (an Arduino plugged into USB has VCC of 4.5 to 5.2V)
float get_light_level()
{
float operatingVoltage = analogRead(REFERENCE_3V3);
float lightSensor = analogRead(LIGHT);
operatingVoltage = 3.3 / operatingVoltage; //The reference voltage is 3.3V
lightSensor = operatingVoltage * lightSensor;
return (lightSensor);
}
//Returns the voltage of the raw pin based on the 3.3V rail
//This allows us to ignore what VCC might be (an Arduino plugged into USB has VCC of 4.5 to 5.2V)
//Battery level is connected to the RAW pin on Arduino and is fed through two 5% resistors:
//3.9K on the high side (R1), and 1K on the low side (R2)
float get_battery_level()
{
float operatingVoltage = analogRead(REFERENCE_3V3);
float rawVoltage = analogRead(BATT);
operatingVoltage = 3.30 / operatingVoltage; //The reference voltage is 3.3V
rawVoltage = operatingVoltage * rawVoltage; //Convert the 0 to 1023 int to actual voltage on BATT pin
rawVoltage *= 4.90; //(3.9k+1k)/1k - multiple BATT voltage by the voltage divider to get actual system voltage
return (rawVoltage);
}
//Returns the instataneous wind speed
float get_wind_speed()
{
float deltaTime = millis() - lastWindCheck; //750ms
deltaTime /= 1000.0; //Covert to seconds
float windSpeed = (float)windClicks / deltaTime; //3 / 0.750s = 4
windClicks = 0; //Reset and start watching for new wind
lastWindCheck = millis();
windSpeed *= 1.492; //4 * 1.492 = 5.968MPH
/* Serial.println();
Serial.print("Windspeed:");
Serial.println(windSpeed);*/
return (windSpeed);
}
//Read the wind direction sensor, return heading in degrees
int get_wind_direction()
{
unsigned int adc;
adc = analogRead(WDIR); // get the current reading from the sensor
// The following table is ADC readings for the wind direction sensor output, sorted from low to high.
// Each threshold is the midpoint between adjacent headings. The output is degrees for that ADC reading.
// Note that these are not in compass degree order! See Weather Meters datasheet for more information.
if (adc < 380) return (113);
if (adc < 393) return (68);
if (adc < 414) return (90);
if (adc < 456) return (158);
if (adc < 508) return (135);
if (adc < 551) return (203);
if (adc < 615) return (180);
if (adc < 680) return (23);
if (adc < 746) return (45);
if (adc < 801) return (248);
if (adc < 833) return (225);
if (adc < 878) return (338);
if (adc < 913) return (0);
if (adc < 940) return (293);
if (adc < 967) return (315);
if (adc < 990) return (270);
return (-1); // error, disconnected?
}
//Prints the various variables directly to the port
//I don't like the way this function is written but Arduino doesn't support floats under sprintf
void printWeather()
{
//Serial.println();
Serial.print("$,winddir=");
Serial.print(winddir);
Serial.print(",windspeedmph=");
Serial.print(windspeedmph, 1);
Serial.print(",humidity=");
Serial.print(humidity, 1);
Serial.print(",tempH=");
Serial.print(tempH, 1);
Serial.print(",tempP=");
Serial.print(tempP, 1);
Serial.print(",rain=");
Serial.print(rain, 2);
Serial.print(",pressure=");
Serial.print(pressure, 2);
Serial.print(",batt_lvl=");
Serial.print(batt_lvl, 2);
Serial.print(",light_lvl=");
Serial.print(light_lvl, 2);
Serial.print(",lat=");
Serial.print(gps.location.lat(), 6);
Serial.print(",lng=");
Serial.print(gps.location.lng(), 6);
Serial.print(",altitude=");
Serial.print(gps.altitude.meters());
Serial.print(",sats=");
Serial.print(gps.satellites.value());
char sz[32];
Serial.print(",date=");
sprintf(sz, "%02d/%02d/%02d", gps.date.month(), gps.date.day(), gps.date.year());
Serial.print(sz);
Serial.print(",time=");
sprintf(sz, "%02d:%02d:%02d", gps.time.hour(), gps.time.minute(), gps.time.second());
Serial.print(sz);
Serial.print(",");
Serial.println("#");
}

11
Weather/Arduino/makefile Normal file
View File

@@ -0,0 +1,11 @@
weather:
mkdir -p bin/
arduino-cli compile -b arduino:avr:uno Weather.ino -o bin/Weather
install:
sudo service weather stop
arduino-cli upload -b arduino:avr:uno -p /dev/ttyACM0 -i bin/Weather
sudo service weather start
clean:
rm -rf bin/

View File

@@ -0,0 +1,5 @@
SET REMOTE=ckaczor@172.23.10.6
plink %REMOTE% mkdir -p Weather/Arduino
pscp -v -r makefile Weather.ino %REMOTE%:Weather/Arduino

View File

@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AssemblyName>Weather.Models</AssemblyName>
<RootNamespace>Weather.Models</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,121 @@
using JetBrains.Annotations;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using System;
using System.Globalization;
using System.Linq;
namespace Weather.Models
{
public enum MessageType
{
Text,
Data
}
[PublicAPI]
public enum WindDirection
{
None = -1,
North = 0,
East = 90,
South = 180,
West = 270,
NorthEast = 45,
SouthEast = 135,
SouthWest = 225,
NorthWest = 315,
NorthNorthEast = 23,
EastNorthEast = 68,
EastSouthEast = 113,
SouthSouthEast = 158,
SouthSouthWest = 203,
WestSouthWest = 248,
WestNorthWest = 293,
NorthNorthWest = 338
}
public class WeatherMessage
{
[JsonConverter(typeof(StringEnumConverter))]
public MessageType Type { get; set; }
public DateTimeOffset Timestamp { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
public WindDirection WindDirection { get; set; }
public double WindSpeed { get; set; }
public double Humidity { get; set; }
public double HumidityTemperature { get; set; }
public double Rain { get; set; }
public double Pressure { get; set; }
public double PressureTemperature { get; set; }
public double BatteryLevel { get; set; }
public double LightLevel { get; set; }
public double Latitude { get; set; }
public double Longitude { get; set; }
public double Altitude { get; set; }
public int SatelliteCount { get; set; }
public DateTimeOffset GpsTimestamp { get; set; }
public string Message { get; set; }
public WeatherMessage()
{
Type = MessageType.Text;
Timestamp = DateTimeOffset.UtcNow;
}
public WeatherMessage(string message)
{
Type = MessageType.Data;
Timestamp = DateTimeOffset.UtcNow;
var messageParts = message.Split(',').ToList();
messageParts.RemoveAt(0);
messageParts.RemoveAt(messageParts.Count - 1);
var messageValues = messageParts.Select(m => m.Split('=')).ToDictionary(a => a[0], a => a[1]);
WindDirection = Enum.Parse<WindDirection>(messageValues[@"winddir"]);
WindSpeed = double.Parse(messageValues[@"windspeedmph"]);
Humidity = double.Parse(messageValues[@"humidity"]);
HumidityTemperature = double.Parse(messageValues[@"tempH"]);
Rain = double.Parse(messageValues[@"rain"]);
Pressure = double.Parse(messageValues[@"pressure"]);
PressureTemperature = double.Parse(messageValues[@"tempP"]);
BatteryLevel = double.Parse(messageValues[@"batt_lvl"]);
LightLevel = double.Parse(messageValues[@"light_lvl"]);
Latitude = double.Parse(messageValues[@"lat"]);
Longitude = double.Parse(messageValues[@"lng"]);
Altitude = double.Parse(messageValues[@"altitude"]);
SatelliteCount = int.Parse(messageValues[@"sats"]);
DateTimeOffset.TryParseExact($"{messageValues[@"date"]} {messageValues[@"time"]}", "MM/dd/yyyy HH:mm:ss", null, DateTimeStyles.None, out var gpsTimestamp);
GpsTimestamp = gpsTimestamp;
}
[PublicAPI]
public static WeatherMessage Parse(string message)
{
if (message.StartsWith("$") && message.EndsWith("#"))
return new WeatherMessage(message);
return new WeatherMessage { Message = message };
}
}
}

View File

@@ -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/Weather/SerialReader/SerialReader.dll"
],
"cwd": "/home/ckaczor/Weather/SerialReader",
"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"
}
]
}

35
Weather/SerialReader/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,35 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/SerialReader.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"
}
]
}

View File

@@ -0,0 +1,174 @@
using Microsoft.Extensions.Configuration;
using Newtonsoft.Json;
using RabbitMQ.Client;
using System;
using System.IO.Ports;
using System.Linq;
using System.Text;
using System.Threading;
using Weather.Models;
namespace Weather.SerialReader
{
internal static class Program
{
private static IConfiguration _configuration;
private static readonly CancellationTokenSource CancellationTokenSource = new CancellationTokenSource();
private static DateTime _lastLogTime = DateTime.MinValue;
private static long _messageCount;
private static bool _boardStarting;
private static void Main()
{
WriteLog("Starting");
Console.CancelKeyPress += OnCancelKeyPress;
_configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", false, false)
.AddEnvironmentVariables()
.Build();
var baudRate = int.Parse(_configuration["Weather:Port:BaudRate"]);
while (!CancellationTokenSource.Token.IsCancellationRequested)
{
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();
_boardStarting = 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}");
}
}
WriteLog("Exiting");
}
private static void ReadSerial(SerialPort serialPort, IModel model)
{
while (!CancellationTokenSource.Token.IsCancellationRequested)
{
try
{
var message = serialPort.ReadLine();
if (!_boardStarting)
{
_boardStarting = message.Contains("Board starting");
if (_boardStarting)
WriteLog("Board starting");
}
if (!_boardStarting)
{
WriteLog($"Message received but board not starting: {message}");
continue;
}
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);
_messageCount++;
if ((DateTime.Now - _lastLogTime).TotalMinutes < 1)
continue;
WriteLog($"Number of messages received since {_lastLogTime} = {_messageCount}");
_lastLogTime = DateTime.Now;
_messageCount = 0;
}
catch (TimeoutException)
{
WriteLog("Serial port read timeout");
}
}
}
private static void OnCancelKeyPress(object sender, ConsoleCancelEventArgs args)
{
args.Cancel = true;
CancellationTokenSource.Cancel();
}
private static string GetPort()
{
var portPrefix = _configuration["Weather:Port:Prefix"];
while (!CancellationTokenSource.Token.IsCancellationRequested)
{
WriteLog($"Checking for port starting with: {portPrefix}");
var ports = SerialPort.GetPortNames();
var port = ports.FirstOrDefault(p => p.StartsWith(portPrefix));
if (port != null)
{
WriteLog($"Port found: {port}");
return port;
}
WriteLog("Port not found - waiting");
Thread.Sleep(1000);
}
return null;
}
private static void WriteLog(string message)
{
Console.WriteLine(message);
}
}
}

View File

@@ -0,0 +1,12 @@
{
"profiles": {
"SerialReader": {
"commandName": "Project",
"launchBrowser": false,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:62648/"
}
}
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
<PropertyGroup>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<AssemblyName>Weather.SerialReader</AssemblyName>
<RootNamespace>Weather.SerialReader</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2019.1.3" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="3.0.0-preview6.19304.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="3.0.0-preview6.19304.6" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.0.0-preview6.19304.6" />
<PackageReference Include="RabbitMQ.Client" Version="5.1.0" />
<PackageReference Include="System.IO.Ports" Version="4.6.0-preview5.19224.8" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ActiveDebugProfile>IIS Express</ActiveDebugProfile>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/AutoDetectedNamingRules/=Locals/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/AutoDetectedNamingRules/=EnumMember/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/AutoDetectedNamingRules/=Property/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Int64 x:Key="/Default/CodeStyle/Naming/CSharpAutoNaming/AutoNamingCompletedVersion/@EntryValue">2</s:Int64></wpf:ResourceDictionary>

View File

@@ -0,0 +1,11 @@
{
"Weather": {
"Port": {
"Prefix": "/dev/ttyACM",
"BaudRate": 9600
},
"Queue": {
"Name": "weather"
}
}
}

View File

@@ -0,0 +1,68 @@
---
kind: StatefulSet
apiVersion: apps/v1
metadata:
name: weather-queue
namespace: home-monitor
labels:
app: weather-queue
spec:
replicas: 1
selector:
matchLabels:
app: weather-queue
serviceName: weather-queue
template:
metadata:
labels:
app: weather-queue
spec:
containers:
- name: weather-queue
image: rabbitmq:3.7.16-management-alpine
terminationMessagePath: "/dev/termination-log"
terminationMessagePolicy: File
imagePullPolicy: IfNotPresent
env:
- name: RABBITMQ_DEFAULT_USER
valueFrom:
secretKeyRef:
name: weather-queue-credentials
key: username
- name: RABBITMQ_DEFAULT_PASS
valueFrom:
secretKeyRef:
name: weather-queue-credentials
key: password
volumeMounts:
- name: data
mountPath: /var/lib/rabbitmq
restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst
nodeSelector:
kubernetes.io/hostname: weather
schedulerName: default-scheduler
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: local-path
resources:
requests:
storage: 1Gi
---
kind: Service
apiVersion: v1
metadata:
name: weather-queue
spec:
ports:
- name: client
port: 5672
- name: http
port: 15672
selector:
app: weather-queue
type: ClusterIP

View File

@@ -0,0 +1,45 @@
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: weather-serial-reader
namespace: home-monitor
labels:
app: weather-serial-reader
spec:
replicas: 1
selector:
matchLabels:
app: weather-serial-reader
template:
metadata:
labels:
app: weather-serial-reader
spec:
containers:
- name: weather-serial-reader
image: ckaczor/home-monitor-weather-serialreader:latest
terminationMessagePath: "/dev/termination-log"
terminationMessagePolicy: File
imagePullPolicy: Always
securityContext:
privileged: true
env:
- name: Weather__Queue__Host
value: weather-queue
- name: Weather__Queue__User
valueFrom:
secretKeyRef:
name: weather-queue-credentials
key: username
- name: Weather__Queue__Password
valueFrom:
secretKeyRef:
name: weather-queue-credentials
key: password
restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst
nodeSelector:
kubernetes.io/hostname: weather
schedulerName: default-scheduler

30
Weather/Service/.vscode/launch.json vendored Normal file
View File

@@ -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/Weather/Service/Weather.Service.dll"
],
"cwd": "/home/ckaczor/Weather/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"
}
]
}

35
Weather/Service/.vscode/tasks.json vendored Normal file
View File

@@ -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"
}
]
}

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic;
namespace Weather.Service.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
[HttpGet]
public ActionResult<IEnumerable<string>> Get()
{
return new[] { "value1", "value2" };
}
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
return "value";
}
[HttpPost]
public void Post([FromBody] string value) { }
[HttpPut("{id}")]
public void Put(int id, [FromBody] string value) { }
[HttpDelete("{id}")]
public void Delete(int id) { }
}
}

View File

@@ -0,0 +1,81 @@
using Dapper;
using Microsoft.Extensions.Configuration;
using Npgsql;
using Weather.Models;
namespace Weather.Service.Data
{
public class Database
{
private readonly IConfiguration _configuration;
public Database(IConfiguration configuration)
{
_configuration = configuration;
}
public void EnsureDatabase()
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder
{
Host = _configuration["Weather:Database:Host"],
Username = _configuration["Weather:Database:User"],
Password = _configuration["Weather:Database:Password"],
Database = "postgres"
};
using (var connection = new NpgsqlConnection(connectionStringBuilder.ConnectionString))
{
var command = new NpgsqlCommand { Connection = connection };
connection.Open();
// Check to see if the database exists
command.CommandText = $"SELECT TRUE from pg_database WHERE datname='{_configuration["Weather:Database:Name"]}'";
var databaseExists = (bool?)command.ExecuteScalar();
// Create database if needed
if (!databaseExists.GetValueOrDefault(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("Weather.Service.Data.Resources.Schema.sql");
// Make sure the database is up to date
command.CommandText = schema;
command.ExecuteNonQuery();
}
}
private NpgsqlConnection CreateConnection()
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder
{
Host = _configuration["Weather:Database:Host"],
Username = _configuration["Weather:Database:User"],
Password = _configuration["Weather:Database:Password"],
Database = _configuration["Weather:Database:Name"]
};
var connection = new NpgsqlConnection(connectionStringBuilder.ConnectionString);
connection.Open();
return connection;
}
public void StoreWeatherData(WeatherMessage weatherMessage)
{
using (var connection = CreateConnection())
{
var query = ResourceReader.GetString("Weather.Service.Data.Resources.CreateReading.sql");
connection.Execute(query, weatherMessage);
}
}
}
}

View File

@@ -0,0 +1,6 @@
INSERT INTO weather_reading (timestamp, wind_direction, wind_speed, humidity, humidity_temperature, rain, pressure,
pressure_temperature, battery_level, light_level, latitude, longitude, altitude,
satellite_count, gps_timestamp)
VALUES (:timestamp, :windDirection, :windSpeed, :humidity, :humidityTemperature, :rain, :pressure, :pressureTemperature,
:batteryLevel, :lightLevel, :latitude, :longitude, :altitude, :satelliteCount, :gpsTimestamp)
ON CONFLICT DO NOTHING

View File

@@ -0,0 +1,24 @@
CREATE EXTENSION IF NOT EXISTS timescaledb CASCADE;
CREATE TABLE IF NOT EXISTS weather_reading
(
timestamp timestamptz NOT NULL
CONSTRAINT weather_reading_pk
PRIMARY KEY,
wind_direction int NOT NULL,
wind_speed double precision NOT NULL,
humidity double precision NOT NULL,
humidity_temperature double precision NOT NULL,
rain double precision NOT NULL,
pressure double precision NOT NULL,
pressure_temperature double precision NOT NULL,
battery_level double precision NOT NULL,
light_level double precision NOT NULL,
latitude double precision NOT NULL,
longitude double precision NOT NULL,
altitude double precision NOT NULL,
satellite_count double precision NOT NULL,
gps_timestamp timestamptz NOT NULL
);
SELECT create_hypertable('weather_reading', 'timestamp', if_not_exists => TRUE);

View File

@@ -0,0 +1,119 @@
using JetBrains.Annotations;
using Microsoft.AspNetCore.SignalR.Client;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Newtonsoft.Json;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Weather.Models;
using Weather.Service.Data;
namespace Weather.Service
{
[UsedImplicitly]
public class MessageHandler : IHostedService
{
private readonly IConfiguration _configuration;
private readonly Database _database;
private IConnection _queueConnection;
private IModel _queueModel;
private HubConnection _hubConnection;
private static DateTime _lastLogTime = DateTime.MinValue;
private static long _messageCount;
public MessageHandler(IConfiguration configuration, Database database)
{
_configuration = configuration;
_database = database;
}
public Task StartAsync(CancellationToken cancellationToken)
{
WriteLog("MessageHandler: Start");
var factory = new ConnectionFactory
{
HostName = _configuration["Weather:Queue:Host"],
UserName = _configuration["Weather:Queue:User"],
Password = _configuration["Weather:Queue:Password"]
};
_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);
_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)
{
var body = eventArgs.Body;
var message = Encoding.UTF8.GetString(body);
_messageCount++;
if ((DateTime.Now - _lastLogTime).TotalMinutes >= 1)
{
WriteLog($"Number of messages received since {_lastLogTime} = {_messageCount}");
_lastLogTime = DateTime.Now;
_messageCount = 0;
}
var weatherMessage = JsonConvert.DeserializeObject<WeatherMessage>(message);
if (weatherMessage.Type == MessageType.Text)
{
WriteLog(weatherMessage.Message);
return;
}
_database.StoreWeatherData(weatherMessage);
try
{
if (_hubConnection.State == HubConnectionState.Disconnected)
_hubConnection.StartAsync().Wait();
_hubConnection.InvokeAsync("SendLatestReading", message).Wait();
}
catch (Exception exception)
{
WriteLog($"Hub exception: {exception}");
}
}
private static void WriteLog(string message)
{
Console.WriteLine(message);
}
}
}

View File

@@ -0,0 +1,22 @@
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
namespace Weather.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<Startup>();
}
}
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"Service": {
"commandName": "Project",
"launchUrl": "api/values",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5001"
}
}
}

View File

@@ -0,0 +1,26 @@
using System;
using System.IO;
using System.Reflection;
namespace Weather.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();
}
}
}
}
}

View File

@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<SuppressNETCoreSdkPreviewMessage>true</SuppressNETCoreSdkPreviewMessage>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
<AssemblyName>Weather.Service</AssemblyName>
<RootNamespace>Weather.Service</RootNamespace>
</PropertyGroup>
<ItemGroup>
<None Remove="Data\Resources\CreateReading.sql" />
<None Remove="Data\Resources\Schema.sql" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Data\Resources\CreateReading.sql" />
<EmbeddedResource Include="Data\Resources\Schema.sql" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="1.60.6" />
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="1.1.0" />
<PackageReference Include="Npgsql" Version="4.0.7" />
<PackageReference Include="RabbitMQ.Client" Version="5.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Models\Models.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DebuggerFlavor>ProjectDebugger</DebuggerFlavor>
</PropertyGroup>
<PropertyGroup>
<ActiveDebugProfile>Service</ActiveDebugProfile>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Weather.Service.Data;
namespace Weather.Service
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddTransient<Database>();
services.AddHostedService<MessageHandler>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(IApplicationBuilder applicationBuilder, IHostingEnvironment environment)
{
if (environment.IsDevelopment())
applicationBuilder.UseDeveloperExceptionPage();
var database = applicationBuilder.ApplicationServices.GetService<Database>();
database.EnsureDatabase();
applicationBuilder.UseMvc();
}
}
}

View File

@@ -0,0 +1,10 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
}
}

View File

@@ -0,0 +1,18 @@
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"Weather": {
"Database": {
"Name": "weather"
},
"Queue": {
"Name": "weather"
}
},
"Hub": {
"Weather": "http://hub-server/weatherHub"
}
}

View File

@@ -0,0 +1,68 @@
---
kind: StatefulSet
apiVersion: apps/v1
metadata:
name: weather-database
namespace: home-monitor
labels:
app: weather-database
spec:
replicas: 1
selector:
matchLabels:
app: weather-database
serviceName: weather-database
template:
metadata:
labels:
app: weather-database
spec:
containers:
- name: weather-database
image: timescale/timescaledb
terminationMessagePath: "/dev/termination-log"
terminationMessagePolicy: File
imagePullPolicy: IfNotPresent
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: weather-database-credentials
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: weather-database-credentials
key: password
- name: POSTGRES_DB
value: weather
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
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: weather-database
spec:
ports:
- name: client
port: 5432
selector:
app: weather-database
type: ClusterIP

View File

@@ -0,0 +1,59 @@
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: weather-service
namespace: home-monitor
labels:
app: weather-service
spec:
replicas: 1
selector:
matchLabels:
app: weather-service
template:
metadata:
labels:
app: weather-service
spec:
containers:
- name: weather-service
image: ckaczor/home-monitor-weather-service:latest
terminationMessagePath: "/dev/termination-log"
terminationMessagePolicy: File
imagePullPolicy: Always
securityContext:
privileged: true
env:
- name: Weather__Queue__Host
value: weather-queue
- name: Weather__Queue__User
valueFrom:
secretKeyRef:
name: weather-queue-credentials
key: username
- name: Weather__Queue__Password
valueFrom:
secretKeyRef:
name: weather-queue-credentials
key: password
- name: Weather__Database__Host
value: weather-database
- name: Weather__Database__User
valueFrom:
secretKeyRef:
name: weather-database-credentials
key: username
- name: Weather__Database__Password
valueFrom:
secretKeyRef:
name: weather-database-credentials
key: password
- name: Hub__Weather
value: http://hub-service/weatherHub
restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst
nodeSelector:
kubernetes.io/hostname: kubernetes
schedulerName: default-scheduler

37
Weather/Weather.sln Normal file
View File

@@ -0,0 +1,37 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.28705.295
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SerialReader", "SerialReader\SerialReader.csproj", "{63142748-213D-4BD3-9A15-8E5C405718B4}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Models", "Models\Models.csproj", "{39E5DE26-CD8D-47AF-AB94-6ACB0AF24EA1}"
EndProject
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

View File

@@ -0,0 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/SPACE_WITHIN_SINGLE_LINE_ARRAY_INITIALIZER_BRACES/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=appsettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -0,0 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/AutoDetectedNamingRules/=Locals/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/AutoDetectedNamingRules/=EnumMember/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/AutoDetectedNamingRules/=Property/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Int64 x:Key="/Default/CodeStyle/Naming/CSharpAutoNaming/AutoNamingCompletedVersion/@EntryValue">2</s:Int64></wpf:ResourceDictionary>