Add initial device status service

This commit is contained in:
2022-08-17 16:27:28 -04:00
parent 054396d242
commit 2d14463132
18 changed files with 648 additions and 0 deletions

View File

@@ -0,0 +1,162 @@
#include <WiFiNINA.h>
#include <ArduinoMqttClient.h>
#include "arduino_secrets.h"
char ssid[] = SECRET_SSID;
char pass[] = SECRET_PASS;
WiFiClient wifiClient;
MqttClient mqttClient(wifiClient);
const char broker[] = "172.23.10.51";
int port = 1883;
const long interval = 500; // Interval for sending messages (milliseconds)
unsigned long previousMilliseconds = 0;
int washerPin = 0;
int dryerPin = 1;
int lastWasherValue = -1;
int lastDryerValue = -1;
const char washerTopic[] = "washer";
const char dryerTopic[] = "dryer";
void connectNetwork() {
Serial.print("Attempting to connect to WPA SSID: ");
Serial.println(ssid);
while (WiFi.begin(ssid, pass) != WL_CONNECTED) {
Serial.print(".");
delay(5000);
}
Serial.println("Connected to the network");
Serial.println();
}
bool checkNetwork() {
if (!WiFi.status() == WL_DISCONNECTED) {
WiFi.disconnect();
Serial.print("Attempting to reconnect to WPA SSID: ");
Serial.println(ssid);
if (!WiFi.begin(ssid, pass) != WL_CONNECTED) {
Serial.println("Network reconnection failed");
return false;
}
Serial.println("Network reconnected");
return true;
}
return true;
}
void connectBroker() {
Serial.print("Attempting to connect to the MQTT broker: ");
Serial.println(broker);
while (!mqttClient.connect(broker, port)) {
Serial.print(".");
delay(5000);
}
Serial.print("Connected to the MQTT broker: ");
Serial.println(broker);
}
bool checkBroker() {
if (!mqttClient.connected()) {
Serial.print("Attempting to reconnect to the MQTT broker: ");
Serial.println(broker);
if (!mqttClient.connect(broker, port)) {
Serial.println("Broker reconnection failed");
return false;
}
Serial.println("Broker reconnected");
outputValue(washerTopic, lastWasherValue);
outputValue(dryerTopic, lastDryerValue);
return true;
}
return true;
}
void setupDevices() {
pinMode(washerPin, INPUT_PULLUP);
pinMode(dryerPin, INPUT_PULLUP);
lastWasherValue = digitalRead(washerPin);
lastDryerValue = digitalRead(dryerPin);
outputValue(washerTopic, lastWasherValue);
outputValue(dryerTopic, lastDryerValue);
}
void outputValue(char topic[], int value) {
Serial.print("Sending message to topic: ");
Serial.print(topic);
Serial.print(" ");
Serial.println(value == 1 ? 0 : 1);
mqttClient.beginMessage(topic);
mqttClient.print(value == 1 ? 0 : 1);
mqttClient.endMessage();
}
void setup() {
Serial.begin(9600);
while (!Serial) {
; // wait for serial port to connect. Needed for native USB port only
}
connectNetwork();
connectBroker();
setupDevices();
Serial.println();
}
void loop() {
mqttClient.poll();
unsigned long currentMilliseconds = millis();
if (currentMilliseconds - previousMilliseconds >= interval) {
previousMilliseconds = currentMilliseconds;
if (!checkNetwork()) {
return;
}
if (!checkBroker()) {
return;
}
int washerValue = digitalRead(washerPin);
if (washerValue != lastWasherValue) {
lastWasherValue = washerValue;
outputValue(washerTopic, washerValue);
}
int dryerValue = digitalRead(dryerPin);
if (dryerValue != lastDryerValue) {
lastDryerValue = dryerValue;
outputValue(dryerTopic, dryerValue);
}
}
}

View File

@@ -0,0 +1,2 @@
#define SECRET_SSID ""
#define SECRET_PASS ""

View File

@@ -0,0 +1,25 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.3.32811.315
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Service", "Service\Service.csproj", "{2EAE02A0-3657-419F-AF3E-BF0B942FAE0D}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2EAE02A0-3657-419F-AF3E-BF0B942FAE0D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2EAE02A0-3657-419F-AF3E-BF0B942FAE0D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2EAE02A0-3657-419F-AF3E-BF0B942FAE0D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2EAE02A0-3657-419F-AF3E-BF0B942FAE0D}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A63ACB0D-1EE1-4C2A-A084-1704568325FE}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,2 @@
<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/UserDictionary/Words/=mqtt/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -0,0 +1,35 @@
{
"version": "0.2.0",
"configurations": [
{
// 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
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/net6.0/Service.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
// Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser
"serverReadyAction": {
"action": "openExternally",
"pattern": "\\bNow listening on:\\s+(https?://\\S+)"
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach"
}
]
}

41
DeviceStatus/Service/.vscode/tasks.json vendored Normal file
View File

@@ -0,0 +1,41 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"build",
"${workspaceFolder}/Service.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "publish",
"command": "dotnet",
"type": "process",
"args": [
"publish",
"${workspaceFolder}/Service.csproj",
"/property:GenerateFullPaths=true",
"/consoleloggerparameters:NoSummary"
],
"problemMatcher": "$msCompile"
},
{
"label": "watch",
"command": "dotnet",
"type": "process",
"args": [
"watch",
"run",
"--project",
"${workspaceFolder}/Service.csproj"
],
"problemMatcher": "$msCompile"
}
]
}

View File

@@ -0,0 +1,21 @@
using Microsoft.AspNetCore.Mvc;
namespace Service.Controllers
{
[Route("[controller]")]
[ApiController]
public class StatusController : ControllerBase
{
private readonly DeviceRepository _deviceRepository;
public StatusController(DeviceRepository deviceRepository)
{
_deviceRepository = deviceRepository;
}
[HttpGet("recent")]
public ActionResult<IEnumerable<Device>> GetRecent()
{
return _deviceRepository.Values;
}
}
}

View File

@@ -0,0 +1,18 @@
namespace Service;
public class Device
{
public string Name { get; }
public bool Status { get; set; }
public Device(string name, string statusString)
{
Name = name;
Update(statusString);
}
public void Update(string statusString)
{
Status = statusString == "1";
}
}

View File

@@ -0,0 +1,18 @@
namespace Service;
public class DeviceRepository : Dictionary<string, Device>
{
public void HandleDeviceMessage(string name, string value)
{
if (ContainsKey(name))
{
this[name].Update(value);
}
else
{
var device = new Device(name, value);
this[name] = device;
}
}
}

View File

@@ -0,0 +1,19 @@
FROM mcr.microsoft.com/dotnet/aspnet:6.0-alpine AS base
WORKDIR /app
EXPOSE 80
EXPOSE 1883
FROM mcr.microsoft.com/dotnet/sdk:6.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
WORKDIR /app
COPY --from=build /app .
RUN apk add --no-cache icu-libs
ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false
ENTRYPOINT ["dotnet", "ChrisKaczor.HomeMonitor.DeviceStatus.Service.dll"]

View File

@@ -0,0 +1,76 @@
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.AspNetCore.SignalR.Client;
using MQTTnet;
using MQTTnet.Server;
namespace Service;
public class MessageHandler : IHostedService
{
private MqttServer? _mqttServer;
private HubConnection? _hubConnection;
private readonly IConfiguration _configuration;
private readonly DeviceRepository _deviceRepository;
public MessageHandler(IConfiguration configuration, DeviceRepository deviceRepository)
{
_configuration = configuration;
_deviceRepository = deviceRepository;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
if (!string.IsNullOrEmpty(_configuration["Hub:DeviceStatus"]))
_hubConnection = new HubConnectionBuilder().WithUrl(_configuration["Hub:DeviceStatus"]).Build();
var mqttFactory = new MqttFactory();
var mqttServerOptions = new MqttServerOptionsBuilder().WithDefaultEndpoint().Build();
_mqttServer = mqttFactory.CreateMqttServer(mqttServerOptions);
_mqttServer.InterceptingPublishAsync += OnInterceptingPublishAsync;
await _mqttServer.StartAsync();
}
private async Task OnInterceptingPublishAsync(InterceptingPublishEventArgs arg)
{
_deviceRepository.HandleDeviceMessage(arg.ApplicationMessage.Topic, arg.ApplicationMessage.ConvertPayloadToString());
Console.WriteLine(arg.ApplicationMessage.Topic);
Console.WriteLine(arg.ApplicationMessage.ConvertPayloadToString());
if (_hubConnection == null)
return;
try
{
if (_hubConnection.State == HubConnectionState.Disconnected)
_hubConnection.StartAsync().Wait();
var json = JsonSerializer.Serialize(_deviceRepository[arg.ApplicationMessage.Topic]);
await _hubConnection.InvokeAsync("SendLatestStatus", json);
}
catch (Exception exception)
{
WriteLog($"Hub exception: {exception}");
}
}
public async Task StopAsync(CancellationToken cancellationToken)
{
if (_hubConnection != null)
await _hubConnection.StopAsync(cancellationToken);
if (_mqttServer != null)
await _mqttServer.StopAsync();
}
private static void WriteLog(string message)
{
Console.WriteLine(message);
}
}

View File

@@ -0,0 +1,26 @@
using Service;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHostedService<MessageHandler>();
builder.Services.AddSingleton<DeviceRepository>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

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

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="JetBrains.Annotations" Version="2022.1.0" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.8" />
<PackageReference Include="MQTTnet" Version="4.1.0.247" />
<PackageReference Include="MQTTnet.AspNetCore" Version="4.1.0.247" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Hub": {
"DeviceStatus": "http://localhost:5000/device-status"
}
}

View File

@@ -0,0 +1,12 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Hub": {
"DeviceStatus": "http://hub-server/device-status"
}
}

View File

@@ -0,0 +1,68 @@
name: $(Rev:r)
pr: none
trigger:
batch: 'true'
branches:
include:
- master
paths:
include:
- DeviceStatus/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: 'DeviceStatus/Service/Dockerfile'
imageName: 'ckaczor/home-monitor-device-status-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-device-status-service:$(Build.BuildNumber)'
includeLatestTag: true
- task: Bash@3
inputs:
targetType: 'inline'
script: 'sed -i s/#BUILD_BUILDNUMBER#/$BUILD_BUILDNUMBER/ DeviceStatus/Service/deploy/manifest.yaml'
- task: PublishBuildArtifacts@1
inputs:
PathtoPublish: 'DeviceStatus/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'

View File

@@ -0,0 +1,65 @@
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: device-status-service
namespace: home-monitor
labels:
app: device-status-service
spec:
replicas: 1
selector:
matchLabels:
app: device-status-service
template:
metadata:
labels:
app: device-status-service
spec:
containers:
- name: device-status-service
image: ckaczor/home-monitor-device-status-service:#BUILD_BUILDNUMBER#
terminationMessagePath: "/dev/termination-log"
terminationMessagePolicy: File
imagePullPolicy: Always
securityContext:
privileged: true
env:
- name: Hub__DeviceStatus
value: http://hub-service/device-status
restartPolicy: Always
terminationGracePeriodSeconds: 30
dnsPolicy: ClusterFirst
nodeSelector:
kubernetes.io/hostname: kubernetes
schedulerName: default-scheduler
---
kind: Service
apiVersion: v1
metadata:
name: device-status-service
spec:
ports:
- name: client
port: 80
selector:
app: device-status-service
type: ClusterIP
---
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
name: device-status
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/device-status"
backend:
serviceName: device-status-service
servicePort: 80