Compare commits
18 Commits
c84427a9b3
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
061785d77b
|
|||
|
c4c4fed4bc
|
|||
|
e44c1c24c5
|
|||
|
df05898a65
|
|||
|
916fcb3584
|
|||
|
d9af0b8b30
|
|||
|
aa666d799a
|
|||
|
3825517dae
|
|||
|
c21695a37e
|
|||
|
3743eefa69
|
|||
|
845d566c60
|
|||
|
99f39b91af
|
|||
|
55ea88a29f
|
|||
|
756fd0bccf
|
|||
|
24f2ad63a7
|
|||
|
ce18c44363
|
|||
|
6472d3cd1e
|
|||
|
3416ce1003
|
@@ -2,6 +2,7 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches: ["main"]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
68
.gitea/workflows/release.yml
Executable file
68
.gitea/workflows/release.yml
Executable file
@@ -0,0 +1,68 @@
|
||||
#
|
||||
name: Create and publish a Docker image
|
||||
|
||||
# Configures this workflow to run every time a change is pushed to the branch called `release`.
|
||||
on:
|
||||
push:
|
||||
branches: ['release']
|
||||
tags:
|
||||
- '*'
|
||||
workflow_dispatch:
|
||||
|
||||
# Defines two custom environment variables for the workflow. These are used for the Container registry domain, and a name for the Docker image that this workflow builds.
|
||||
env:
|
||||
REGISTRY: git.housh.dev
|
||||
IMAGE_NAME: ${{ gitea.repository }}
|
||||
|
||||
# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
runs-on: ubuntu-latest
|
||||
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
#
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
# Uses the `docker/login-action` action to log in to the Container registry registry using the account and password that will publish the packages. Once published, the packages are scoped to the account defined here.
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ gitea.actor }}
|
||||
password: ${{ secrets.CONTAINER_TOKEN }}
|
||||
# This step uses [docker/metadata-action](https://github.com/docker/metadata-action#about) to extract tags and labels that will be applied to the specified image. The `id` "meta" allows the output of this step to be referenced in a subsequent step. The `images` value provides the base name for the tags and labels.
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
type=sha
|
||||
# This step uses the `docker/build-push-action` action to build the image, based on your repository's `Dockerfile`. If the build succeeds, it pushes the image to GitHub Packages.
|
||||
# It uses the `context` parameter to define the build's context as the set of files located in the specified path. For more information, see "[Usage](https://github.com/docker/build-push-action#usage)" in the README of the `docker/build-push-action` repository.
|
||||
# It uses the `tags` and `labels` parameters to tag and label the image with the output from the "meta" step.
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# This step generates an artifact attestation for the image, which is an unforgeable statement about where and how it was built. It increases supply chain security for people who consume the image. For more information, see "[AUTOTITLE](/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds)."
|
||||
# - name: Generate artifact attestation
|
||||
# uses: actions/attest-build-provenance@v1
|
||||
# with:
|
||||
# subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
||||
# push-to-registry: true
|
||||
# github-token: ${{ secrets.CONTAINER_TOKEN }}
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,8 +5,10 @@
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.dewPoint-env
|
||||
.dewPoint-env*
|
||||
.topics
|
||||
mqtt_password.txt
|
||||
.env
|
||||
.smbdelete*
|
||||
buildServer.json
|
||||
.nvim/*
|
||||
|
||||
9
.swiftlint.yml
Normal file
9
.swiftlint.yml
Normal file
@@ -0,0 +1,9 @@
|
||||
disabled_rules:
|
||||
- closing_brace
|
||||
- fuction_body_length
|
||||
|
||||
included:
|
||||
- Sources
|
||||
- Tests
|
||||
|
||||
ignore_multiline_statement_conditions: true
|
||||
28
.swiftpm/dewpoint-controller-Package.xctestplan
Normal file
28
.swiftpm/dewpoint-controller-Package.xctestplan
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"configurations" : [
|
||||
{
|
||||
"id" : "AFB1047B-4742-43D2-AFB9-680C1CB2D273",
|
||||
"name" : "Test Scheme Action",
|
||||
"options" : {
|
||||
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultOptions" : {
|
||||
"targetForVariableExpansion" : {
|
||||
"containerPath" : "container:",
|
||||
"identifier" : "dewpoint-controller",
|
||||
"name" : "dewpoint-controller"
|
||||
}
|
||||
},
|
||||
"testTargets" : [
|
||||
{
|
||||
"target" : {
|
||||
"containerPath" : "container:",
|
||||
"identifier" : "IntegrationTests",
|
||||
"name" : "IntegrationTests"
|
||||
}
|
||||
}
|
||||
],
|
||||
"version" : 1
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Bootstrap"
|
||||
BuildableName = "Bootstrap"
|
||||
BlueprintName = "Bootstrap"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Bootstrap"
|
||||
BuildableName = "Bootstrap"
|
||||
BlueprintName = "Bootstrap"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Client"
|
||||
BuildableName = "Client"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Client"
|
||||
BuildableName = "Client"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ClientLive"
|
||||
BuildableName = "ClientLive"
|
||||
BlueprintName = "ClientLive"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ClientLive"
|
||||
BuildableName = "ClientLive"
|
||||
BlueprintName = "ClientLive"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DewPointEnvironment"
|
||||
BuildableName = "DewPointEnvironment"
|
||||
BlueprintName = "DewPointEnvironment"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DewPointEnvironment"
|
||||
BuildableName = "DewPointEnvironment"
|
||||
BlueprintName = "DewPointEnvironment"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EnvVars"
|
||||
BuildableName = "EnvVars"
|
||||
BlueprintName = "EnvVars"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EnvVars"
|
||||
BuildableName = "EnvVars"
|
||||
BlueprintName = "EnvVars"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,67 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Models"
|
||||
BuildableName = "Models"
|
||||
BlueprintName = "Models"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Models"
|
||||
BuildableName = "Models"
|
||||
BlueprintName = "Models"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,256 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Bootstrap"
|
||||
BuildableName = "Bootstrap"
|
||||
BlueprintName = "Bootstrap"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Client"
|
||||
BuildableName = "Client"
|
||||
BlueprintName = "Client"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ClientLive"
|
||||
BuildableName = "ClientLive"
|
||||
BlueprintName = "ClientLive"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "DewPointEnvironment"
|
||||
BuildableName = "DewPointEnvironment"
|
||||
BlueprintName = "DewPointEnvironment"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "EnvVars"
|
||||
BuildableName = "EnvVars"
|
||||
BlueprintName = "EnvVars"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "Models"
|
||||
BuildableName = "Models"
|
||||
BlueprintName = "Models"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controller"
|
||||
BuildableName = "dewPoint-controller"
|
||||
BlueprintName = "dewPoint-controller"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ClientTests"
|
||||
BuildableName = "ClientTests"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controllerTests"
|
||||
BuildableName = "dewPoint-controllerTests"
|
||||
BlueprintName = "dewPoint-controllerTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "TopicsLive"
|
||||
BuildableName = "TopicsLive"
|
||||
BlueprintName = "TopicsLive"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "SensorsService"
|
||||
BuildableName = "SensorsService"
|
||||
BlueprintName = "SensorsService"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ClientTests"
|
||||
BuildableName = "ClientTests"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controllerTests"
|
||||
BuildableName = "dewPoint-controllerTests"
|
||||
BlueprintName = "dewPoint-controllerTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "MQTTConnectionServiceTests"
|
||||
BuildableName = "MQTTConnectionServiceTests"
|
||||
BlueprintName = "MQTTConnectionServiceTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "SensorsServiceTests"
|
||||
BuildableName = "SensorsServiceTests"
|
||||
BlueprintName = "SensorsServiceTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controller"
|
||||
BuildableName = "dewPoint-controller"
|
||||
BlueprintName = "dewPoint-controller"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controller"
|
||||
BuildableName = "dewPoint-controller"
|
||||
BlueprintName = "dewPoint-controller"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
@@ -1,126 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1330"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controller"
|
||||
BuildableName = "dewPoint-controller"
|
||||
BlueprintName = "dewPoint-controller"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ClientTests"
|
||||
BuildableName = "ClientTests"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "NO"
|
||||
buildForArchiving = "NO"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controllerTests"
|
||||
BuildableName = "dewPoint-controllerTests"
|
||||
BlueprintName = "dewPoint-controllerTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "ClientTests"
|
||||
BuildableName = "ClientTests"
|
||||
BlueprintName = "ClientTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
<TestableReference
|
||||
skipped = "NO">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controllerTests"
|
||||
BuildableName = "dewPoint-controllerTests"
|
||||
BlueprintName = "dewPoint-controllerTests"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controller"
|
||||
BuildableName = "dewPoint-controller"
|
||||
BlueprintName = "dewPoint-controller"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "dewPoint-controller"
|
||||
BuildableName = "dewPoint-controller"
|
||||
BlueprintName = "dewPoint-controller"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
29
Makefile
29
Makefile
@@ -1,22 +1,39 @@
|
||||
DOCKER_IMAGE_NAME?="swift-mqtt-dewpoint"
|
||||
DOCKER_TAG_NAME?="latest"
|
||||
|
||||
bootstrap-env:
|
||||
.PHONY: bootstrap
|
||||
bootstrap:
|
||||
@cp Bootstrap/dewPoint-env-example .dewPoint-env
|
||||
|
||||
bootstrap-topics:
|
||||
@cp Bootstrap/topics-example .topics
|
||||
|
||||
bootstrap: bootstrap-env bootstrap-topics
|
||||
|
||||
.PHONY: build
|
||||
build:
|
||||
@swift build -Xswiftc -strict-concurrency=complete
|
||||
|
||||
.PHONY: build-docker
|
||||
build-docker:
|
||||
@docker build \
|
||||
--file docker/Dockerfile \
|
||||
--tag "${DOCKER_IMAGE_NAME}:${DOCKER_TAG_NAME}" .
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
rm -rf .build
|
||||
|
||||
.PHONY: run
|
||||
run:
|
||||
@swift run dewpoint-controller
|
||||
|
||||
.PHONY: test-docker
|
||||
test-docker:
|
||||
@docker compose --file docker/docker-compose-test.yaml \
|
||||
run --build --remove-orphans -i --rm test
|
||||
@docker compose --file docker/docker-compose-test.yaml down
|
||||
|
||||
.PHONY: start-mosquitto
|
||||
start-mosquitto:
|
||||
@docker compose --file docker/docker-compose.yaml \
|
||||
up -d mosquitto
|
||||
|
||||
.PHONY: test-swift
|
||||
test-swift: start-mosquitto
|
||||
@swift test --enable-code-coverage
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "33fdcea7245de36c7e638047a16bba6605bc9bac0117aab7cb9397289a33214e",
|
||||
"originHash" : "486be5d69e4f0ba7b9f42046df31a727c7e394e4ecfae5671e1b194bed7c9e9b",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "combine-schedulers",
|
||||
@@ -10,6 +10,15 @@
|
||||
"version" : "1.0.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "dotenv",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/swiftpackages/DotEnv.git",
|
||||
"state" : {
|
||||
"revision" : "1f15bb9de727d694af1d003a1a5d7a553752850f",
|
||||
"version" : "3.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "mqtt-nio",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -19,6 +28,15 @@
|
||||
"version" : "2.11.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-argument-parser",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/apple/swift-argument-parser.git",
|
||||
"state" : {
|
||||
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
|
||||
"version" : "1.5.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-async-algorithms",
|
||||
"kind" : "remoteSourceControl",
|
||||
@@ -64,6 +82,15 @@
|
||||
"version" : "1.3.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-custom-dump",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/pointfreeco/swift-custom-dump",
|
||||
"state" : {
|
||||
"revision" : "82645ec760917961cfa08c9c0c7104a57a0fa4b1",
|
||||
"version" : "1.3.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"identity" : "swift-dependencies",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
||||
@@ -14,30 +14,53 @@ let package = Package(
|
||||
],
|
||||
products: [
|
||||
.executable(name: "dewpoint-controller", targets: ["DewPointController"]),
|
||||
.library(name: "CliClient", targets: ["CliClient"]),
|
||||
.library(name: "Models", targets: ["Models"]),
|
||||
.library(name: "MQTTManager", targets: ["MQTTManager"]),
|
||||
.library(name: "MQTTConnectionService", targets: ["MQTTConnectionService"]),
|
||||
.library(name: "SensorsService", targets: ["SensorsService"])
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.0"),
|
||||
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-nio", from: "2.0.0"),
|
||||
.package(url: "https://github.com/apple/swift-log", from: "1.6.0"),
|
||||
.package(url: "https://github.com/swiftpackages/DotEnv.git", from: "3.0.0"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-dependencies", from: "1.5.2"),
|
||||
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.0.0"),
|
||||
.package(url: "https://github.com/swift-psychrometrics/swift-psychrometrics", exact: "0.2.3"),
|
||||
.package(url: "https://github.com/swift-server-community/mqtt-nio.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.3.0")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CliClient",
|
||||
dependencies: [
|
||||
"Models",
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
.product(name: "DotEnv", package: "DotEnv"),
|
||||
.product(name: "MQTTNIO", package: "mqtt-nio")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "CliClientTests",
|
||||
dependencies: [
|
||||
"CliClient"
|
||||
],
|
||||
resources: [
|
||||
.copy("test.env"),
|
||||
.copy("test-env.json")
|
||||
]
|
||||
),
|
||||
.executableTarget(
|
||||
name: "DewPointController",
|
||||
dependencies: [
|
||||
"Models",
|
||||
"MQTTManager",
|
||||
"CliClient",
|
||||
"MQTTConnectionService",
|
||||
"SensorsService",
|
||||
.product(name: "MQTTNIO", package: "mqtt-nio"),
|
||||
.product(name: "NIO", package: "swift-nio"),
|
||||
.product(name: "ArgumentParser", package: "swift-argument-parser"),
|
||||
.product(name: "CustomDump", package: "swift-custom-dump"),
|
||||
// .product(name: "DotEnv", package: "DotEnv"),
|
||||
.product(name: "PsychrometricClientLive", package: "swift-psychrometrics")
|
||||
]
|
||||
),
|
||||
@@ -69,10 +92,13 @@ let package = Package(
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.testTarget(
|
||||
name: "MQTTConnectionServiceTests",
|
||||
name: "IntegrationTests",
|
||||
dependencies: [
|
||||
"DewPointController",
|
||||
"MQTTConnectionService",
|
||||
"MQTTManager",
|
||||
"SensorsService",
|
||||
.product(name: "PsychrometricClientLive", package: "swift-psychrometrics"),
|
||||
.product(name: "ServiceLifecycleTestKit", package: "swift-service-lifecycle")
|
||||
]
|
||||
),
|
||||
@@ -84,16 +110,10 @@ let package = Package(
|
||||
.product(name: "Dependencies", package: "swift-dependencies"),
|
||||
.product(name: "DependenciesMacros", package: "swift-dependencies"),
|
||||
.product(name: "MQTTNIO", package: "mqtt-nio"),
|
||||
.product(name: "PsychrometricClient", package: "swift-psychrometrics"),
|
||||
.product(name: "ServiceLifecycle", package: "swift-service-lifecycle")
|
||||
],
|
||||
swiftSettings: swiftSettings
|
||||
),
|
||||
.testTarget(
|
||||
name: "SensorsServiceTests",
|
||||
dependencies: [
|
||||
"SensorsService",
|
||||
.product(name: "PsychrometricClientLive", package: "swift-psychrometrics")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
224
Sources/CliClient/CliClient.swift
Normal file
224
Sources/CliClient/CliClient.swift
Normal file
@@ -0,0 +1,224 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import DotEnv
|
||||
import Foundation
|
||||
import Logging
|
||||
import Models
|
||||
import MQTTNIO
|
||||
import NIO
|
||||
|
||||
public extension DependencyValues {
|
||||
var cliClient: CliClient {
|
||||
get { self[CliClient.self] }
|
||||
set { self[CliClient.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the interface needed for the command line application.
|
||||
///
|
||||
///
|
||||
@DependencyClient
|
||||
public struct CliClient {
|
||||
|
||||
/// Parse a log level from the given `EnvVars`.
|
||||
public var logLevel: @Sendable (EnvVars) -> Logger.Level = { _ in .debug }
|
||||
|
||||
/// Generate the `EnvVars` with the given parameters.
|
||||
public var makeEnvVars: @Sendable (EnvVarsRequest) async throws -> EnvVars
|
||||
|
||||
/// Generate the `MQTTClient` with the given parameters.
|
||||
public var makeClient: @Sendable (ClientRequest) throws -> MQTTClient
|
||||
|
||||
/// Attempt to parse a string to an `MQTTClient.Version`.
|
||||
public var parseMqttClientVersion: @Sendable (String) -> MQTTClient.Version?
|
||||
|
||||
/// Represents the parameters needed to create an `MQTTClient`.
|
||||
///
|
||||
public struct ClientRequest: Sendable {
|
||||
|
||||
/// The environment variables used to create the client.
|
||||
public let environment: EnvVars
|
||||
|
||||
/// The event loop group for the client.
|
||||
public let eventLoopGroup: MultiThreadedEventLoopGroup
|
||||
|
||||
/// A logger to use with the client.
|
||||
public let logger: Logger?
|
||||
|
||||
/// Create a new client request.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - environment: The environment variables to use.
|
||||
/// - eventLoopGroup: The event loop group to use on the client.
|
||||
/// - logger: An optional logger to use on the client.
|
||||
public init(
|
||||
environment: EnvVars,
|
||||
eventLoopGroup: MultiThreadedEventLoopGroup,
|
||||
logger: Logger?
|
||||
) {
|
||||
self.environment = environment
|
||||
self.eventLoopGroup = eventLoopGroup
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct EnvVarsRequest: Sendable {
|
||||
|
||||
public let envFilePath: String?
|
||||
public let logger: Logger?
|
||||
public let mqttClientVersion: String?
|
||||
|
||||
public init(
|
||||
envFilePath: String? = nil,
|
||||
logger: Logger? = nil,
|
||||
version mqttClientVersion: String? = nil
|
||||
) {
|
||||
self.envFilePath = envFilePath
|
||||
self.logger = logger
|
||||
self.mqttClientVersion = mqttClientVersion
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension CliClient: DependencyKey {
|
||||
public static let testValue: CliClient = Self()
|
||||
public static var liveValue: CliClient {
|
||||
Self(
|
||||
logLevel: { Logger.Level.from(environment: $0) },
|
||||
makeEnvVars: {
|
||||
try await EnvVars.load(
|
||||
dotEnvFile: $0.envFilePath,
|
||||
logger: $0.logger,
|
||||
version: $0.mqttClientVersion
|
||||
)
|
||||
},
|
||||
makeClient: {
|
||||
MQTTClient(
|
||||
environment: $0.environment,
|
||||
eventLoopGroup: $0.eventLoopGroup,
|
||||
logger: $0.logger
|
||||
)
|
||||
},
|
||||
parseMqttClientVersion: { .init(string: $0) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
extension EnvironmentDependency {
|
||||
func dotEnvDict(path: String?) async throws -> [String: String] {
|
||||
guard let path,
|
||||
let file = FileType(path: path)
|
||||
else { return [:] }
|
||||
return try await load(file)
|
||||
}
|
||||
}
|
||||
|
||||
extension EnvVars {
|
||||
|
||||
/// Load the `EnvVars` from the environment.
|
||||
///
|
||||
/// - Paramaters:
|
||||
/// - dotEnvFile: An optional environment file to load.
|
||||
/// - logger: An optional logger to use for debugging.
|
||||
/// - version: A version that is specified from command line, ignoring any environment variable.
|
||||
static func load(
|
||||
dotEnvFile: String?,
|
||||
logger: Logger?,
|
||||
version: String?
|
||||
) async throws -> EnvVars {
|
||||
@Dependency(\.environment) var environment
|
||||
|
||||
let defaultEnvVars = EnvVars()
|
||||
let coders = environment.coders()
|
||||
|
||||
let defaultEnvDict = (try? coders.encode(defaultEnvVars))
|
||||
.flatMap { try? coders.decode([String: String].self, from: $0) }
|
||||
?? [:]
|
||||
|
||||
let dotEnvDict = try await environment.dotEnvDict(path: dotEnvFile)
|
||||
let envVarsDict = defaultEnvDict
|
||||
.merging(environment.processInfo(), uniquingKeysWith: { $1 })
|
||||
.merging(dotEnvDict, uniquingKeysWith: { $1 })
|
||||
|
||||
var envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict))
|
||||
.flatMap { try? coders.decode(EnvVars.self, from: $0) }
|
||||
?? defaultEnvVars
|
||||
|
||||
if let version {
|
||||
envVars.version = version
|
||||
}
|
||||
|
||||
logger?.debug("Done loading EnvVars...")
|
||||
|
||||
return envVars
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension MQTTClient {
|
||||
convenience init(
|
||||
environment envVars: EnvVars,
|
||||
eventLoopGroup: EventLoopGroup,
|
||||
logger: Logger?
|
||||
) {
|
||||
self.init(
|
||||
host: envVars.host,
|
||||
port: envVars.port != nil ? Int(envVars.port!) : nil,
|
||||
identifier: envVars.identifier,
|
||||
eventLoopGroupProvider: .shared(eventLoopGroup),
|
||||
logger: logger,
|
||||
configuration: .init(
|
||||
version: .parseOrDefault(string: envVars.version),
|
||||
disablePing: false,
|
||||
userName: envVars.userName,
|
||||
password: envVars.password
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@_spi(Internal)
|
||||
public extension MQTTClient.Version {
|
||||
static let `default` = Self.v3_1_1
|
||||
|
||||
static func parseOrDefault(string: String?) -> Self {
|
||||
guard let string, let value = Self(string: string) else {
|
||||
return .default
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
init?(string: String) {
|
||||
if string.contains("5") {
|
||||
self = .v5_0
|
||||
} else if string.contains("3") {
|
||||
self = .v3_1_1
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension Logger.Level {
|
||||
|
||||
/// Parse a `Logger.Level` from the loaded `EnvVars`.
|
||||
static func from(environment envVars: EnvVars) -> Self {
|
||||
// If the log level was set via an environment variable.
|
||||
if let logLevel = envVars.logLevel {
|
||||
return logLevel
|
||||
}
|
||||
// Parse the appEnv to derive an log level.
|
||||
switch envVars.appEnv {
|
||||
case .staging, .development:
|
||||
return .debug
|
||||
case .production:
|
||||
return .info
|
||||
case .testing:
|
||||
return .trace
|
||||
}
|
||||
}
|
||||
}
|
||||
134
Sources/CliClient/EnvironmentDependency.swift
Normal file
134
Sources/CliClient/EnvironmentDependency.swift
Normal file
@@ -0,0 +1,134 @@
|
||||
import Dependencies
|
||||
import DependenciesMacros
|
||||
import DotEnv
|
||||
import Foundation
|
||||
import Models
|
||||
|
||||
@_spi(Internal)
|
||||
public extension DependencyValues {
|
||||
|
||||
/// A dependecy responsible for loding environment variables.
|
||||
///
|
||||
/// This is just used internally of this module, but is useful to
|
||||
/// override for testing purposes, so import using `@_spi(Internal)`.
|
||||
var environment: EnvironmentDependency {
|
||||
get { self[EnvironmentDependency.self] }
|
||||
set { self[EnvironmentDependency.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Responsible for loading environment variables and files.
|
||||
///
|
||||
///
|
||||
@_spi(Internal)
|
||||
@DependencyClient
|
||||
public struct EnvironmentDependency: Sendable {
|
||||
|
||||
public var coders: @Sendable () -> any Coderable = { JSONCoders() }
|
||||
|
||||
/// Load the variables based on the request.
|
||||
public var load: @Sendable (FileType) async throws -> [String: String] = { _ in [:] }
|
||||
|
||||
/// Load the environment variables based on the current process environment.
|
||||
///
|
||||
/// You can override this to use an empty environment, which is useful for testing purposes.
|
||||
public var processInfo: @Sendable () -> [String: String] = { [:] }
|
||||
|
||||
/// Represents the types of files that can be loaded and decoded into
|
||||
/// the environment.
|
||||
public enum FileType: Equatable {
|
||||
case dotEnv(path: String)
|
||||
case json(path: String)
|
||||
|
||||
public init?(path: String) {
|
||||
let strings = path.split(separator: ".")
|
||||
guard let ext = strings.last else {
|
||||
return nil
|
||||
}
|
||||
switch ext {
|
||||
case "env":
|
||||
self = .dotEnv(path: path)
|
||||
case "json":
|
||||
self = .json(path: path)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DecodeError: Error {}
|
||||
|
||||
@_spi(Internal)
|
||||
extension EnvironmentDependency: DependencyKey {
|
||||
|
||||
public static let testValue: EnvironmentDependency = Self()
|
||||
|
||||
public static func live(
|
||||
decoder: JSONDecoder = .init(),
|
||||
encoder: JSONEncoder = .init()
|
||||
) -> Self {
|
||||
Self(
|
||||
coders: { JSONCoders(decoder: decoder, encoder: encoder) },
|
||||
load: { file in
|
||||
switch file {
|
||||
case let .dotEnv(path: path):
|
||||
let file = try DotEnv.read(path: path)
|
||||
return file.lines.reduce(into: [String: String]()) { partialResult, line in
|
||||
partialResult[line.key] = line.value
|
||||
}
|
||||
case let .json(path: path):
|
||||
let url = url(for: path)
|
||||
return try decoder.decode(
|
||||
[String: String].self,
|
||||
from: Data(contentsOf: url)
|
||||
)
|
||||
}
|
||||
},
|
||||
processInfo: { ProcessInfo.processInfo.environment }
|
||||
)
|
||||
}
|
||||
|
||||
public static let liveValue: EnvironmentDependency = .live()
|
||||
}
|
||||
|
||||
/// A type that encode and decode values.
|
||||
///
|
||||
/// This is really just here to override tests with coders that will throw an error,
|
||||
/// instead of encoding or decoding data.
|
||||
///
|
||||
@_spi(Internal)
|
||||
public protocol Coderable {
|
||||
func encode<T: Encodable>(_ instance: T) throws -> Data
|
||||
func decode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T
|
||||
}
|
||||
|
||||
struct JSONCoders: Coderable {
|
||||
|
||||
let decoder: JSONDecoder
|
||||
let encoder: JSONEncoder
|
||||
|
||||
init(
|
||||
decoder: JSONDecoder = .init(),
|
||||
encoder: JSONEncoder = .init()
|
||||
) {
|
||||
self.decoder = decoder
|
||||
self.encoder = encoder
|
||||
}
|
||||
|
||||
func encode<T>(_ instance: T) throws -> Data where T: Encodable {
|
||||
try encoder.encode(instance)
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
|
||||
try decoder.decode(T.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
private func url(for path: String) -> URL {
|
||||
#if os(Linux)
|
||||
return URL(fileURLWithPath: path)
|
||||
#else
|
||||
return URL(filePath: path)
|
||||
#endif
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import ArgumentParser
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Logging
|
||||
@@ -11,106 +12,10 @@ import SensorsService
|
||||
import ServiceLifecycle
|
||||
|
||||
@main
|
||||
struct Application {
|
||||
|
||||
/// The main entry point of the application.
|
||||
static func main() async throws {
|
||||
let eventloopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
var logger = Logger(label: "dewpoint-controller")
|
||||
logger.logLevel = .trace
|
||||
|
||||
logger.info("Starting dewpoint-controller!")
|
||||
|
||||
let environment = loadEnvVars(logger: logger)
|
||||
|
||||
if environment.appEnv == .production {
|
||||
logger.debug("Updating logging level to info.")
|
||||
logger.logLevel = .info
|
||||
}
|
||||
|
||||
let mqtt = MQTTClient(
|
||||
envVars: environment,
|
||||
eventLoopGroup: eventloopGroup,
|
||||
logger: logger
|
||||
struct Application: AsyncParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "dewpoint-controller",
|
||||
abstract: "Command for running the dewpoint mqtt service.",
|
||||
subcommands: [Run.self, Debug.self]
|
||||
)
|
||||
|
||||
do {
|
||||
try await withDependencies {
|
||||
$0.psychrometricClient = .liveValue
|
||||
$0.mqtt = .live(client: mqtt, logger: logger)
|
||||
} operation: {
|
||||
let mqttConnection = MQTTConnectionService(logger: logger)
|
||||
let sensors = SensorsService(sensors: .live, logger: logger)
|
||||
|
||||
var serviceGroupConfiguration = ServiceGroupConfiguration(
|
||||
services: [
|
||||
mqttConnection,
|
||||
sensors
|
||||
],
|
||||
gracefulShutdownSignals: [.sigterm, .sigint],
|
||||
logger: logger
|
||||
)
|
||||
serviceGroupConfiguration.maximumCancellationDuration = .seconds(5)
|
||||
serviceGroupConfiguration.maximumGracefulShutdownDuration = .seconds(10)
|
||||
|
||||
let serviceGroup = ServiceGroup(configuration: serviceGroupConfiguration)
|
||||
|
||||
try await serviceGroup.run()
|
||||
}
|
||||
|
||||
try await mqtt.shutdown()
|
||||
try await eventloopGroup.shutdownGracefully()
|
||||
} catch {
|
||||
try await eventloopGroup.shutdownGracefully()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func loadEnvVars(logger: Logger) -> EnvVars {
|
||||
let defaultEnvVars = EnvVars()
|
||||
let encoder = JSONEncoder()
|
||||
let decoder = JSONDecoder()
|
||||
|
||||
let defaultEnvDict = (try? encoder.encode(defaultEnvVars))
|
||||
.flatMap { try? decoder.decode([String: String].self, from: $0) }
|
||||
?? [:]
|
||||
|
||||
let envVarsDict = defaultEnvDict
|
||||
.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 })
|
||||
|
||||
let envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict))
|
||||
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
|
||||
?? defaultEnvVars
|
||||
|
||||
logger.debug("Done loading EnvVars...")
|
||||
|
||||
return envVars
|
||||
}
|
||||
|
||||
private extension MQTTNIO.MQTTClient {
|
||||
convenience init(envVars: EnvVars, eventLoopGroup: EventLoopGroup, logger: Logger?) {
|
||||
self.init(
|
||||
host: envVars.host,
|
||||
port: envVars.port != nil ? Int(envVars.port!) : nil,
|
||||
identifier: envVars.identifier,
|
||||
eventLoopGroupProvider: .shared(eventLoopGroup),
|
||||
logger: logger,
|
||||
configuration: .init(
|
||||
version: .v3_1_1,
|
||||
disablePing: false,
|
||||
userName: envVars.userName,
|
||||
password: envVars.password
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Array where Element == TemperatureAndHumiditySensor {
|
||||
static var live: Self {
|
||||
TemperatureAndHumiditySensor.Location.allCases.map { location in
|
||||
TemperatureAndHumiditySensor(location: location)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
74
Sources/DewPointController/DebugCommand.swift
Normal file
74
Sources/DewPointController/DebugCommand.swift
Normal file
@@ -0,0 +1,74 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import CustomDump
|
||||
import Dependencies
|
||||
import DotEnv
|
||||
import Foundation
|
||||
import Logging
|
||||
import Models
|
||||
|
||||
extension Application {
|
||||
|
||||
struct Debug: AsyncParsableCommand {
|
||||
|
||||
static let configuration: CommandConfiguration = .init(
|
||||
commandName: "debug",
|
||||
abstract: "Debug the environment variables and command line arguments."
|
||||
)
|
||||
|
||||
@OptionGroup
|
||||
var options: SharedOptions
|
||||
|
||||
@Flag(
|
||||
name: [.customLong("show-password")],
|
||||
help: "Don't redact the password from the console."
|
||||
)
|
||||
var showPassword: Bool = false
|
||||
|
||||
mutating func run() async throws {
|
||||
@Dependency(\.cliClient) var client
|
||||
let logger = Logger(label: "debug-command")
|
||||
|
||||
print("--------------------------")
|
||||
print("Running debug command...")
|
||||
if let envFile = options.envFile {
|
||||
print("Reading env file: \(envFile)")
|
||||
print("--------------------------")
|
||||
} else {
|
||||
print("No env file set.")
|
||||
print("--------------------------")
|
||||
}
|
||||
|
||||
print("Loading EnvVars")
|
||||
print("--------------------------")
|
||||
let envVars = try await client.makeEnvVars(options.envVarsRequest(logger: logger))
|
||||
printEnvVars(envVars: envVars, showPassword: showPassword)
|
||||
print("--------------------------")
|
||||
|
||||
if let logLevel = options.logLevel {
|
||||
print("Log Level option: \(logLevel)")
|
||||
print("--------------------------")
|
||||
} else {
|
||||
print("Log Level option: nil")
|
||||
print("--------------------------")
|
||||
}
|
||||
}
|
||||
|
||||
private func printEnvVars(envVars: EnvVars, showPassword: Bool) {
|
||||
// show the proper password to show depending on if it exists
|
||||
// and if we should redact it or not.
|
||||
var passwordString: String?
|
||||
switch (showPassword, envVars.password) {
|
||||
case (true, .none), (_, .none):
|
||||
break
|
||||
case (true, let .some(password)):
|
||||
passwordString = password
|
||||
case (false, .some):
|
||||
passwordString = "<redacted>"
|
||||
}
|
||||
var envVars = envVars
|
||||
envVars.password = passwordString
|
||||
customDump(envVars)
|
||||
}
|
||||
}
|
||||
}
|
||||
90
Sources/DewPointController/RunCommand.swift
Normal file
90
Sources/DewPointController/RunCommand.swift
Normal file
@@ -0,0 +1,90 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Logging
|
||||
import Models
|
||||
import MQTTConnectionService
|
||||
import MQTTManager
|
||||
import MQTTNIO
|
||||
import NIO
|
||||
import PsychrometricClientLive
|
||||
import SensorsService
|
||||
import ServiceLifecycle
|
||||
|
||||
extension Application {
|
||||
/// Run the controller.
|
||||
///
|
||||
struct Run: AsyncParsableCommand {
|
||||
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "run",
|
||||
abstract: "Run the controller."
|
||||
)
|
||||
|
||||
@OptionGroup
|
||||
var options: SharedOptions
|
||||
|
||||
mutating func run() async throws {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
|
||||
let (mqtt, logger) = try await cliClient.setupRun(options: options)
|
||||
logger.info("Setting up environment...")
|
||||
|
||||
do {
|
||||
try await withDependencies {
|
||||
$0.psychrometricClient = .liveValue
|
||||
$0.mqtt = .live(client: mqtt, logger: logger)
|
||||
} operation: {
|
||||
let mqttConnection = MQTTConnectionService(logger: logger)
|
||||
let sensors = SensorsService(sensors: .live, logger: logger)
|
||||
|
||||
var serviceGroupConfiguration = ServiceGroupConfiguration(
|
||||
services: [
|
||||
mqttConnection,
|
||||
sensors
|
||||
],
|
||||
gracefulShutdownSignals: [.sigterm, .sigint],
|
||||
logger: logger
|
||||
)
|
||||
// These settings prevent services from running forever after we've
|
||||
// received a shutdown signal. In general it should not needed unless the
|
||||
// services don't shutdown their async streams properly.
|
||||
serviceGroupConfiguration.maximumCancellationDuration = .seconds(5)
|
||||
serviceGroupConfiguration.maximumGracefulShutdownDuration = .seconds(10)
|
||||
|
||||
let serviceGroup = ServiceGroup(configuration: serviceGroupConfiguration)
|
||||
|
||||
logger.info("Starting dewpoint-controller!")
|
||||
try await serviceGroup.run()
|
||||
}
|
||||
|
||||
// Here we've received a shutdown signal and shutdown all the
|
||||
// services.
|
||||
try await mqtt.shutdown()
|
||||
} catch {
|
||||
// If something fails, shutdown the mqtt client.
|
||||
try await mqtt.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension CliClient {
|
||||
func setupRun(
|
||||
eventLoopGroup: MultiThreadedEventLoopGroup = .init(numberOfThreads: 1),
|
||||
loggerLabel: String = "dewpoint-controller",
|
||||
options: Application.SharedOptions
|
||||
) async throws -> (MQTTClient, Logger) {
|
||||
var logger = Logger(label: loggerLabel)
|
||||
let environment = try await makeEnvVars(options.envVarsRequest(logger: logger))
|
||||
logger.logLevel = logLevel(environment)
|
||||
let client = try makeClient(.init(
|
||||
environment: environment,
|
||||
eventLoopGroup: eventLoopGroup,
|
||||
logger: logger
|
||||
))
|
||||
|
||||
return (client, logger)
|
||||
}
|
||||
}
|
||||
52
Sources/DewPointController/SharedOptions.swift
Normal file
52
Sources/DewPointController/SharedOptions.swift
Normal file
@@ -0,0 +1,52 @@
|
||||
import ArgumentParser
|
||||
import CliClient
|
||||
import Logging
|
||||
import Models
|
||||
import MQTTNIO
|
||||
|
||||
extension Application {
|
||||
|
||||
struct SharedOptions: ParsableArguments {
|
||||
@Option(
|
||||
name: [.short, .customLong("env-file")],
|
||||
help: "A file path to an env file."
|
||||
)
|
||||
var envFile: String?
|
||||
|
||||
@Option(
|
||||
name: [.short, .customLong("log-level")],
|
||||
help: "Set the logging level."
|
||||
)
|
||||
var logLevelContainer: LogLevelContainer?
|
||||
|
||||
@Option(
|
||||
name: [.short, .long],
|
||||
help: "Set the MQTT connecition version."
|
||||
)
|
||||
var version: String?
|
||||
|
||||
func envVarsRequest(logger: Logger?) -> CliClient.EnvVarsRequest {
|
||||
.init(envFilePath: envFile, logger: logger, version: version)
|
||||
}
|
||||
|
||||
var logLevel: Logger.Level? { logLevelContainer?.logLevel }
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// A container type for making `Logger.Level` into a type
|
||||
/// that can be parsed as a command line argument. This is
|
||||
/// to suppress warnings vs. having `Logger.Level` adopt the
|
||||
/// protocol.
|
||||
@_spi(Internal)
|
||||
public struct LogLevelContainer: ExpressibleByArgument {
|
||||
public let logLevel: Logger.Level?
|
||||
|
||||
public init?(argument: String) {
|
||||
self.logLevel = .init(rawValue: argument.lowercased())
|
||||
}
|
||||
|
||||
public func callAsFunction() -> Logger.Level? {
|
||||
logLevel
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,14 @@ import MQTTManager
|
||||
import ServiceLifecycle
|
||||
|
||||
public struct MQTTConnectionService: Service {
|
||||
@Dependency(\.mqtt) var mqtt
|
||||
|
||||
private let logger: Logger?
|
||||
|
||||
public init(
|
||||
logger: Logger? = nil
|
||||
) {
|
||||
var logger = logger
|
||||
logger?[metadataKey: "type"] = "mqtt-connection-service"
|
||||
self.logger = logger
|
||||
}
|
||||
|
||||
@@ -19,19 +20,19 @@ public struct MQTTConnectionService: Service {
|
||||
/// to the MQTT broker and handles graceful shutdown of the
|
||||
/// connection.
|
||||
public func run() async throws {
|
||||
try await withGracefulShutdownHandler {
|
||||
@Dependency(\.mqtt) var mqtt
|
||||
try await mqtt.connect()
|
||||
|
||||
try await withGracefulShutdownHandler {
|
||||
for await event in try mqtt.connectionStream().cancelOnGracefulShutdown() {
|
||||
// We don't really need to do anything with the events, so just logging
|
||||
// for now. But we need to iterate on an async stream for the service to
|
||||
// continue to run and handle graceful shutdowns.
|
||||
logger?.trace("Received connection event: \(event)")
|
||||
}
|
||||
// when we reach here we are shutting down, so we shutdown
|
||||
// the manager.
|
||||
mqtt.shutdown()
|
||||
} onGracefulShutdown: {
|
||||
self.logger?.trace("Received graceful shutdown.")
|
||||
mqtt.shutdown()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,16 @@ import NIO
|
||||
public extension DependencyValues {
|
||||
|
||||
/// A dependency that is responsible for managing the connection to
|
||||
/// an MQTT broker.
|
||||
/// an MQTT broker, listen to topics, and publish values back to the
|
||||
/// broker.
|
||||
var mqtt: MQTTManager {
|
||||
get { self[MQTTManager.self] }
|
||||
set { self[MQTTManager.self] = newValue }
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents the interface needed for the ``MQTTConnectionService``.
|
||||
/// Represents the interface needed to connect, listen, and publish to an MQTT broker.
|
||||
///
|
||||
/// See ``MQTTConnectionManagerLive`` module for live implementation.
|
||||
@DependencyClient
|
||||
public struct MQTTManager: Sendable {
|
||||
|
||||
@@ -28,6 +28,8 @@ public struct MQTTManager: Sendable {
|
||||
public var connect: @Sendable () async throws -> Void
|
||||
|
||||
/// Create a stream of connection events.
|
||||
///
|
||||
/// - SeeAlso: ``Event``
|
||||
public var connectionStream: @Sendable () throws -> AsyncStream<Event>
|
||||
|
||||
private var _listen: @Sendable ([String], MQTTQoS) async throws -> ListenStream
|
||||
@@ -38,10 +40,24 @@ public struct MQTTManager: Sendable {
|
||||
/// Shutdown the connection to the MQTT broker.
|
||||
public var shutdown: @Sendable () -> Void
|
||||
|
||||
/// Perform an operation with the underlying MQTTClient, this can be useful in
|
||||
/// tests, so this module needs imported with `@_spi(Testing) import` to use this method.
|
||||
private var _withClient: @Sendable ((MQTTClient) async throws -> Void) async throws -> Void
|
||||
|
||||
public init(
|
||||
connect: @escaping @Sendable () async throws -> Void,
|
||||
connectionStream: @escaping @Sendable () throws -> AsyncStream<MQTTManager.Event>,
|
||||
listen: @escaping @Sendable ([String], MQTTQoS) async throws -> MQTTManager.ListenStream,
|
||||
publish: @escaping @Sendable (MQTTManager.PublishRequest) async throws -> Void,
|
||||
shutdown: @escaping @Sendable () -> Void,
|
||||
withClient: @escaping @Sendable ((MQTTClient) async throws -> Void) async throws -> Void = { _ in unimplemented() }
|
||||
) {
|
||||
self.connect = connect
|
||||
self.connectionStream = connectionStream
|
||||
self._listen = listen
|
||||
self.publish = publish
|
||||
self.shutdown = shutdown
|
||||
self._withClient = withClient
|
||||
}
|
||||
|
||||
/// Create an async stream that listens for changes to the given topics.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -77,18 +93,20 @@ public struct MQTTManager: Sendable {
|
||||
_ payload: ByteBuffer,
|
||||
to topicName: String,
|
||||
qos: MQTTQoS,
|
||||
retain: Bool = false
|
||||
retain: Bool = false,
|
||||
properties: MQTTProperties = .init()
|
||||
) async throws {
|
||||
try await publish(.init(
|
||||
topicName: topicName,
|
||||
payload: payload,
|
||||
qos: qos,
|
||||
retain: retain
|
||||
retain: retain,
|
||||
properties: properties
|
||||
))
|
||||
}
|
||||
|
||||
/// Perform an operation with the underlying MQTTClient, this can be useful in
|
||||
/// tests, so this module needs imported with `@_spi(Testing) import` to use this method.
|
||||
/// tests, so this module needs imported with `@_spi(Internal) import MQTTManager` to use this method.
|
||||
@_spi(Internal)
|
||||
public func withClient(
|
||||
_ callback: @Sendable (MQTTClient) async throws -> Void
|
||||
@@ -98,7 +116,7 @@ public struct MQTTManager: Sendable {
|
||||
|
||||
/// Represents connection events that clients can listen for and
|
||||
/// react accordingly.
|
||||
public enum Event: Sendable {
|
||||
public enum Event: Equatable, Sendable {
|
||||
case connected
|
||||
case disconnected
|
||||
case shuttingDown
|
||||
@@ -106,7 +124,7 @@ public struct MQTTManager: Sendable {
|
||||
|
||||
/// Represents the parameters required to publish a new value to the
|
||||
/// MQTT broker.
|
||||
public struct PublishRequest: Equatable, Sendable {
|
||||
public struct PublishRequest: Sendable {
|
||||
|
||||
/// The topic to publish the new value to.
|
||||
public let topicName: String
|
||||
@@ -120,6 +138,8 @@ public struct MQTTManager: Sendable {
|
||||
/// The retain flag for the request.
|
||||
public let retain: Bool
|
||||
|
||||
public let properties: MQTTProperties
|
||||
|
||||
/// Create a new publish request.
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -131,12 +151,14 @@ public struct MQTTManager: Sendable {
|
||||
topicName: String,
|
||||
payload: ByteBuffer,
|
||||
qos: MQTTQoS,
|
||||
retain: Bool
|
||||
retain: Bool,
|
||||
properties: MQTTProperties
|
||||
) {
|
||||
self.topicName = topicName
|
||||
self.payload = payload
|
||||
self.qos = qos
|
||||
self.retain = retain
|
||||
self.properties = properties
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,7 +186,7 @@ public extension MQTTManager {
|
||||
.removeDuplicates()
|
||||
.eraseToStream()
|
||||
},
|
||||
_listen: { topics, qos in
|
||||
listen: { topics, qos in
|
||||
try await manager.listen(to: topics, qos: qos)
|
||||
},
|
||||
publish: { request in
|
||||
@@ -174,18 +196,20 @@ public extension MQTTManager {
|
||||
return
|
||||
}
|
||||
logger?.trace("Begin publishing to topic: \(topic)")
|
||||
defer { logger?.trace("Done publishing to topic: \(topic)") }
|
||||
defer { logger?.debug("Done publishing to topic: \(topic)") }
|
||||
try await client.publish(
|
||||
to: request.topicName,
|
||||
payload: request.payload,
|
||||
qos: request.qos,
|
||||
retain: request.retain
|
||||
)
|
||||
retain: request.retain,
|
||||
properties: request.properties
|
||||
).get()
|
||||
},
|
||||
shutdown: {
|
||||
Task { try await client.shutdown() }
|
||||
manager.shutdown()
|
||||
},
|
||||
_withClient: { callback in
|
||||
withClient: { callback in
|
||||
try await callback(client)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -107,42 +107,6 @@ actor TopicListenerStream {
|
||||
onShutdownHandler = { task.cancel() }
|
||||
}
|
||||
|
||||
// TODO: remove.
|
||||
func listen(
|
||||
_ topics: [String],
|
||||
_ qos: MQTTQoS = .atLeastOnce
|
||||
) async throws -> Stream {
|
||||
var sleepTimes = 0
|
||||
|
||||
while !client.isActive() {
|
||||
guard sleepTimes < 10 else {
|
||||
throw TopicListenerError.connectionTimeout
|
||||
}
|
||||
try? await Task.sleep(for: .milliseconds(100))
|
||||
sleepTimes += 1
|
||||
}
|
||||
|
||||
client.logger.trace("Client is active, begin subscribing to topics.")
|
||||
|
||||
try await subscribe()
|
||||
|
||||
client.logger.trace("Done subscribing, begin listening to topics.")
|
||||
|
||||
client.addPublishListener(named: name) { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
self.logger?.error("Received error while listening: \(error)")
|
||||
case let .success(publishInfo):
|
||||
if topics.contains(publishInfo.topicName) {
|
||||
self.logger?.debug("Recieved new value for topic: \(publishInfo.topicName)")
|
||||
self.continuation.yield(publishInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
private func setIsShuttingDown() {
|
||||
shuttingDown = true
|
||||
onShutdownHandler = nil
|
||||
|
||||
@@ -25,6 +25,12 @@ public struct EnvVars: Codable, Equatable, Sendable {
|
||||
/// The MQTT user password.
|
||||
public var password: String?
|
||||
|
||||
/// Set a custom logging level.
|
||||
public var logLevel: Logger.Level?
|
||||
|
||||
/// Set the mqtt broker version.
|
||||
public var version: String?
|
||||
|
||||
/// Create a new ``EnvVars``
|
||||
///
|
||||
/// - Parameters:
|
||||
@@ -38,9 +44,11 @@ public struct EnvVars: Codable, Equatable, Sendable {
|
||||
appEnv: AppEnv = .development,
|
||||
host: String = "127.0.0.1",
|
||||
port: String? = "1883",
|
||||
identifier: String = "dewPoint-controller",
|
||||
identifier: String = "dewpoint-controller",
|
||||
userName: String? = "mqtt_user",
|
||||
password: String? = "secret!"
|
||||
password: String? = "secret!",
|
||||
logLevel: Logger.Level? = nil,
|
||||
version: String? = nil
|
||||
) {
|
||||
self.appEnv = appEnv
|
||||
self.host = host
|
||||
@@ -48,6 +56,8 @@ public struct EnvVars: Codable, Equatable, Sendable {
|
||||
self.identifier = identifier
|
||||
self.userName = userName
|
||||
self.password = password
|
||||
self.logLevel = logLevel
|
||||
self.version = version
|
||||
}
|
||||
|
||||
/// Custom coding keys.
|
||||
@@ -58,6 +68,8 @@ public struct EnvVars: Codable, Equatable, Sendable {
|
||||
case identifier = "MQTT_IDENTIFIER"
|
||||
case userName = "MQTT_USERNAME"
|
||||
case password = "MQTT_PASSWORD"
|
||||
case logLevel = "LOG_LEVEL"
|
||||
case version = "MQTT_VERSION"
|
||||
}
|
||||
|
||||
/// Represents the different app environments.
|
||||
|
||||
@@ -60,7 +60,10 @@ public struct TemperatureAndHumiditySensor: Identifiable, Sendable {
|
||||
!temperature.value.isNaN,
|
||||
!humidity.value.isNaN
|
||||
else { return nil }
|
||||
return try? await psychrometrics.dewPoint(.dryBulb(temperature, relativeHumidity: humidity))
|
||||
return try? await psychrometrics.dewPoint(.dryBulb(
|
||||
.fahrenheit(temperature.fahrenheit),
|
||||
relativeHumidity: humidity
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +76,7 @@ public struct TemperatureAndHumiditySensor: Identifiable, Sendable {
|
||||
!humidity.value.isNaN
|
||||
else { return nil }
|
||||
return try? await psychrometrics.enthalpy.moistAir(
|
||||
.dryBulb(temperature, relativeHumidity: humidity, altitude: altitude)
|
||||
.dryBulb(.fahrenheit(temperature.fahrenheit), relativeHumidity: humidity, altitude: altitude, units: .imperial)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -81,12 +84,11 @@ public struct TemperatureAndHumiditySensor: Identifiable, Sendable {
|
||||
/// Check whether any of the sensor values have changed and need processed.
|
||||
///
|
||||
/// - Note: Setting a value will set to both the temperature and humidity properties.
|
||||
public var needsProcessed: Bool {
|
||||
get { $temperature.needsProcessed || $humidity.needsProcessed }
|
||||
set {
|
||||
$temperature.needsProcessed = newValue
|
||||
$humidity.needsProcessed = newValue
|
||||
}
|
||||
public var needsProcessed: Bool { $temperature.needsProcessed || $humidity.needsProcessed }
|
||||
|
||||
public mutating func setHasProcessed() {
|
||||
$temperature.setHasProcessed()
|
||||
$humidity.setHasProcessed()
|
||||
}
|
||||
|
||||
/// Represents the different locations of a temperature and humidity sensor, which can
|
||||
@@ -139,3 +141,11 @@ public struct TemperatureAndHumiditySensor: Identifiable, Sendable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Array where Element == TemperatureAndHumiditySensor {
|
||||
static var live: Self {
|
||||
TemperatureAndHumiditySensor.Location.allCases.map {
|
||||
TemperatureAndHumiditySensor(location: $0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,24 +5,20 @@
|
||||
@propertyWrapper
|
||||
public struct TrackedChanges<Value: Sendable>: Sendable {
|
||||
|
||||
/// The current tracking state.
|
||||
private var tracking: TrackingState
|
||||
|
||||
/// The current wrapped value.
|
||||
private var value: Value
|
||||
/// The current value wrapped in a tracking state.
|
||||
private var value: TrackingState
|
||||
|
||||
/// Used to check if a new value is equal to an old value.
|
||||
private var isEqual: @Sendable (Value, Value) -> Bool
|
||||
|
||||
/// Access to the underlying property that we are wrapping.
|
||||
public var wrappedValue: Value {
|
||||
get { value }
|
||||
get { value.currentValue }
|
||||
set {
|
||||
// Check if the new value is equal to the old value.
|
||||
guard !isEqual(newValue, value) else { return }
|
||||
guard !isEqual(newValue, value.currentValue) else { return }
|
||||
// If it's not equal then set it, as well as set the tracking to `.needsProcessed`.
|
||||
value = newValue
|
||||
tracking = .needsProcessed
|
||||
value = .needsProcessed(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,31 +33,42 @@ public struct TrackedChanges<Value: Sendable>: Sendable {
|
||||
needsProcessed: Bool = false,
|
||||
isEqual: @escaping @Sendable (Value, Value) -> Bool
|
||||
) {
|
||||
self.value = wrappedValue
|
||||
self.tracking = needsProcessed ? .needsProcessed : .hasProcessed
|
||||
self.value = .init(wrappedValue, needsProcessed: needsProcessed)
|
||||
self.isEqual = isEqual
|
||||
}
|
||||
|
||||
/// Represents whether a wrapped value has changed and needs processed or not.
|
||||
enum TrackingState {
|
||||
fileprivate enum TrackingState {
|
||||
case hasProcessed(Value)
|
||||
case needsProcessed(Value)
|
||||
|
||||
/// The state when nothing has changed and we've already processed the current value.
|
||||
case hasProcessed
|
||||
init(_ value: Value, needsProcessed: Bool) {
|
||||
if needsProcessed {
|
||||
self = .needsProcessed(value)
|
||||
} else {
|
||||
self = .hasProcessed(value)
|
||||
}
|
||||
}
|
||||
|
||||
/// The state when the value has changed and has not been processed yet.
|
||||
case needsProcessed
|
||||
var currentValue: Value {
|
||||
switch self {
|
||||
case let .hasProcessed(value):
|
||||
return value
|
||||
case let .needsProcessed(value):
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
var needsProcessed: Bool {
|
||||
guard case .needsProcessed = self else { return false }
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether the value needs processed.
|
||||
public var needsProcessed: Bool {
|
||||
get { tracking == .needsProcessed }
|
||||
set {
|
||||
if newValue {
|
||||
tracking = .needsProcessed
|
||||
} else {
|
||||
tracking = .hasProcessed
|
||||
}
|
||||
}
|
||||
var needsProcessed: Bool { value.needsProcessed }
|
||||
|
||||
mutating func setHasProcessed() {
|
||||
value = .hasProcessed(value.currentValue)
|
||||
}
|
||||
|
||||
public var projectedValue: Self {
|
||||
@@ -95,6 +102,5 @@ extension TrackedChanges: Hashable where Value: Hashable {
|
||||
|
||||
public func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(wrappedValue)
|
||||
hasher.combine(needsProcessed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ public actor SensorsService: Service {
|
||||
|
||||
private func publishUpdates() async throws {
|
||||
for sensor in sensors.filter(\.needsProcessed) {
|
||||
try await publish(sensor.dewPoint?.value, to: sensor.topics.dewPoint)
|
||||
try await publish(sensor.dewPoint?.fahrenheit, to: sensor.topics.dewPoint)
|
||||
try await publish(sensor.enthalpy?.value, to: sensor.topics.enthalpy)
|
||||
try sensors.hasProcessed(sensor)
|
||||
}
|
||||
@@ -172,7 +172,7 @@ private extension Array where Element == TemperatureAndHumiditySensor {
|
||||
guard let index = firstIndex(where: { $0.id == sensor.id }) else {
|
||||
throw SensorNotFoundError()
|
||||
}
|
||||
self[index].needsProcessed = false
|
||||
self[index].setHasProcessed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +207,6 @@ extension Humidity<Relative>: BufferInitalizable {
|
||||
extension Temperature<DryAir>: BufferInitalizable {
|
||||
init?(buffer: ByteBuffer) {
|
||||
guard let value = Double(buffer: buffer) else { return nil }
|
||||
self.init(value)
|
||||
self.init(value, units: .celsius)
|
||||
}
|
||||
}
|
||||
|
||||
212
Tests/CliClientTests/CliClientTests.swift
Normal file
212
Tests/CliClientTests/CliClientTests.swift
Normal file
@@ -0,0 +1,212 @@
|
||||
@_spi(Internal) import CliClient
|
||||
import Dependencies
|
||||
import Foundation
|
||||
import Logging
|
||||
import Models
|
||||
import MQTTNIO
|
||||
import XCTest
|
||||
|
||||
final class CliClientTests: XCTestCase {
|
||||
|
||||
override func invokeTest() {
|
||||
withDependencies {
|
||||
$0.cliClient = .liveValue
|
||||
$0.environment = .liveValue
|
||||
$0.environment.processInfo = { [:] }
|
||||
} operation: {
|
||||
super.invokeTest()
|
||||
}
|
||||
}
|
||||
|
||||
func testParsingMQTTVersion() {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
|
||||
let arguments = [
|
||||
(MQTTClient.Version.v3_1_1, ["3", "3.1", "3.1.1", "00367894"]),
|
||||
(MQTTClient.Version.v5_0, ["5", "5.1", "5.1.1", "00000500012"]),
|
||||
(nil, ["0", "0.1", "0.1.1", "0000000001267894", "blob"])
|
||||
]
|
||||
|
||||
for (version, strings) in arguments {
|
||||
for string in strings {
|
||||
XCTAssertEqual(cliClient.parseMqttClientVersion(string), version)
|
||||
}
|
||||
}
|
||||
|
||||
XCTAssertEqual(MQTTClient.Version.parseOrDefault(string: nil), .v3_1_1)
|
||||
}
|
||||
|
||||
func testLogLevelFromEnvironment() {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
|
||||
let arguments = [
|
||||
(Logger.Level.debug, EnvVars(appEnv: .staging, logLevel: nil)),
|
||||
(Logger.Level.debug, EnvVars(appEnv: .development, logLevel: nil)),
|
||||
(Logger.Level.info, EnvVars(appEnv: .production, logLevel: nil)),
|
||||
(Logger.Level.trace, EnvVars(appEnv: .testing, logLevel: nil)),
|
||||
(Logger.Level.info, EnvVars(appEnv: .staging, logLevel: .info)),
|
||||
(Logger.Level.trace, EnvVars(appEnv: .development, logLevel: .trace)),
|
||||
(Logger.Level.warning, EnvVars(appEnv: .production, logLevel: .warning)),
|
||||
(Logger.Level.debug, EnvVars(appEnv: .testing, logLevel: .debug))
|
||||
]
|
||||
|
||||
for (expected, envVars) in arguments {
|
||||
XCTAssertEqual(expected, cliClient.logLevel(envVars))
|
||||
}
|
||||
}
|
||||
|
||||
func testMakeEnvVars() async throws {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
@Dependency(\.environment) var environment
|
||||
|
||||
let arguments = [
|
||||
(
|
||||
CliClient.EnvVarsRequest(envFilePath: nil, logger: nil, version: nil),
|
||||
EnvVars()
|
||||
),
|
||||
(
|
||||
CliClient.EnvVarsRequest(envFilePath: nil, logger: nil, version: "3"),
|
||||
EnvVars(version: "3")
|
||||
),
|
||||
(
|
||||
CliClient.EnvVarsRequest(
|
||||
envFilePath: "test.env", // Needs to be a bundled resource.
|
||||
logger: nil,
|
||||
version: nil
|
||||
),
|
||||
EnvVars.test
|
||||
),
|
||||
(
|
||||
CliClient.EnvVarsRequest(
|
||||
envFilePath: "test-env.json", // Needs to be a bundled resource.
|
||||
logger: nil,
|
||||
version: nil
|
||||
),
|
||||
EnvVars.test
|
||||
)
|
||||
]
|
||||
|
||||
for (request, expectedEnvVars) in arguments {
|
||||
var request = request
|
||||
if let file = request.envFilePath {
|
||||
request = .init(
|
||||
envFilePath: cleanFilePath(file),
|
||||
logger: request.logger,
|
||||
version: request.mqttClientVersion
|
||||
)
|
||||
}
|
||||
let result = try await cliClient.makeEnvVars(request)
|
||||
XCTAssertEqual(result, expectedEnvVars)
|
||||
}
|
||||
}
|
||||
|
||||
func testMakeEnvVarsWithFailingDecoder() async throws {
|
||||
try await withDependencies {
|
||||
$0.environment.coders = { ThrowingDecoder() }
|
||||
} operation: {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
let envVars = try await cliClient.makeEnvVars(.init())
|
||||
XCTAssertEqual(envVars, EnvVars())
|
||||
}
|
||||
}
|
||||
|
||||
func testMakeEnvVarsWithFailingEncoder() async throws {
|
||||
try await withDependencies {
|
||||
$0.environment.coders = { ThrowingEncoder() }
|
||||
} operation: {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
let envVars = try await cliClient.makeEnvVars(.init())
|
||||
XCTAssertEqual(envVars, EnvVars())
|
||||
}
|
||||
}
|
||||
|
||||
func testFileType() {
|
||||
let arguments = [
|
||||
(EnvironmentDependency.FileType.dotEnv(path: "test.env"), "test.env"),
|
||||
(EnvironmentDependency.FileType.json(path: "test.json"), "test.json"),
|
||||
(nil, "test"),
|
||||
(nil, "")
|
||||
]
|
||||
|
||||
for (expected, file) in arguments {
|
||||
XCTAssertEqual(EnvironmentDependency.FileType(path: file), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func testEnvironmentLiveValueProcessInfo() {
|
||||
let environment = EnvironmentDependency.liveValue
|
||||
XCTAssertEqual(environment.processInfo(), ProcessInfo.processInfo.environment)
|
||||
}
|
||||
|
||||
func testMakeClient() throws {
|
||||
@Dependency(\.cliClient) var cliClient
|
||||
let envVars = EnvVars.test
|
||||
let client = try cliClient.makeClient(.init(
|
||||
environment: envVars,
|
||||
eventLoopGroup: .init(numberOfThreads: 1),
|
||||
logger: nil
|
||||
))
|
||||
|
||||
XCTAssertEqual(client.host, envVars.host)
|
||||
XCTAssertEqual(client.port, Int(envVars.port!))
|
||||
XCTAssertEqual(client.identifier, envVars.identifier)
|
||||
XCTAssertEqual(client.configuration.version, .v5_0)
|
||||
XCTAssertEqual(client.configuration.userName, envVars.userName)
|
||||
XCTAssertEqual(client.configuration.password, envVars.password)
|
||||
|
||||
try client.syncShutdownGracefully()
|
||||
}
|
||||
}
|
||||
|
||||
// - MARK: Helper
|
||||
|
||||
private func cleanFilePath(_ path: String) -> String {
|
||||
#if os(Linux)
|
||||
return "Tests/CliClientTests/\(path)"
|
||||
#else
|
||||
let split = path.split(separator: ".")
|
||||
let fileName = split.first!
|
||||
let ext = split.last!
|
||||
let url = Bundle.module.url(forResource: String(fileName), withExtension: String(ext))!.absoluteString
|
||||
let cleaned = url.split(separator: "file://").last!
|
||||
return String(cleaned)
|
||||
#endif
|
||||
}
|
||||
|
||||
extension EnvVars {
|
||||
static let test = EnvVars(
|
||||
appEnv: .testing,
|
||||
host: "test.mqtt",
|
||||
port: "1234",
|
||||
identifier: "testing-mqtt",
|
||||
userName: "test-user",
|
||||
password: "super-secret",
|
||||
logLevel: .debug,
|
||||
version: "5.0"
|
||||
)
|
||||
}
|
||||
|
||||
struct ThrowingDecoder: Coderable {
|
||||
|
||||
func encode<T>(_ instance: T) throws -> Data where T: Encodable {
|
||||
try JSONEncoder().encode(instance)
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
|
||||
throw DecodeError()
|
||||
}
|
||||
}
|
||||
|
||||
struct ThrowingEncoder: Coderable {
|
||||
|
||||
func encode<T>(_ instance: T) throws -> Data where T: Encodable {
|
||||
throw EncodeError()
|
||||
}
|
||||
|
||||
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T: Decodable {
|
||||
try JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
}
|
||||
|
||||
struct DecodeError: Error {}
|
||||
struct EncodeError: Error {}
|
||||
10
Tests/CliClientTests/test-env.json
Executable file
10
Tests/CliClientTests/test-env.json
Executable file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"APP_ENV": "testing",
|
||||
"MQTT_HOST": "test.mqtt",
|
||||
"MQTT_PORT": "1234",
|
||||
"MQTT_IDENTIFIER": "testing-mqtt",
|
||||
"MQTT_USERNAME": "test-user",
|
||||
"MQTT_PASSWORD": "super-secret",
|
||||
"LOG_LEVEL": "debug",
|
||||
"MQTT_VERSION": "5.0"
|
||||
}
|
||||
8
Tests/CliClientTests/test.env
Normal file
8
Tests/CliClientTests/test.env
Normal file
@@ -0,0 +1,8 @@
|
||||
APP_ENV="testing"
|
||||
MQTT_HOST="test.mqtt"
|
||||
MQTT_PORT="1234"
|
||||
MQTT_IDENTIFIER="testing-mqtt"
|
||||
MQTT_USERNAME="test-user"
|
||||
MQTT_PASSWORD="super-secret"
|
||||
LOG_LEVEL="debug"
|
||||
MQTT_VERSION="5.0"
|
||||
88
Tests/SensorsServiceTests/SensorsClientTests.swift → Tests/IntegrationTests/IntegrationTests.swift
Executable file → Normal file
88
Tests/SensorsServiceTests/SensorsClientTests.swift → Tests/IntegrationTests/IntegrationTests.swift
Executable file → Normal file
@@ -1,26 +1,29 @@
|
||||
import Dependencies
|
||||
|
||||
// @_spi(Internal) import dewpoint_controller
|
||||
import Logging
|
||||
import Models
|
||||
import MQTTConnectionService
|
||||
@_spi(Internal) import MQTTManager
|
||||
import MQTTNIO
|
||||
import NIO
|
||||
import PsychrometricClientLive
|
||||
@_spi(Internal) import SensorsService
|
||||
import ServiceLifecycle
|
||||
import ServiceLifecycleTestKit
|
||||
import XCTest
|
||||
|
||||
final class SensorsClientTests: XCTestCase {
|
||||
|
||||
final class IntegrationTests: XCTestCase {
|
||||
static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost"
|
||||
|
||||
static let logger: Logger = {
|
||||
var logger = Logger(label: "SensorsClientTests")
|
||||
logger.logLevel = .trace
|
||||
var logger = Logger(label: "IntegrationTests")
|
||||
logger.logLevel = .info
|
||||
return logger
|
||||
}()
|
||||
|
||||
override func invokeTest() {
|
||||
let client = createClient(identifier: "\(Self.self)")
|
||||
|
||||
withDependencies {
|
||||
$0.mqtt = .live(client: client, logger: Self.logger)
|
||||
$0.psychrometricClient = PsychrometricClient.liveValue
|
||||
@@ -29,6 +32,74 @@ final class SensorsClientTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testConnectionServiceShutdown() async throws {
|
||||
@Dependency(\.mqtt) var mqtt
|
||||
do {
|
||||
let service = MQTTConnectionService(logger: Self.logger)
|
||||
let task = Task { try await service.run() }
|
||||
defer { task.cancel() }
|
||||
|
||||
try await Task.sleep(for: .milliseconds(200))
|
||||
|
||||
// check the connection is active here.
|
||||
try await mqtt.withClient { client in
|
||||
XCTAssert(client.isActive())
|
||||
}
|
||||
mqtt.shutdown()
|
||||
|
||||
try await Task.sleep(for: .milliseconds(500))
|
||||
// check the connection is active here.
|
||||
try await mqtt.withClient { client in
|
||||
XCTAssertFalse(client.isActive())
|
||||
}
|
||||
} catch {
|
||||
mqtt.shutdown()
|
||||
try await Task.sleep(for: .milliseconds(500))
|
||||
}
|
||||
}
|
||||
|
||||
func testMQTTConnectionStream() async throws {
|
||||
let client = createClient(identifier: "testNonManagedStream")
|
||||
let manager = MQTTManager.live(
|
||||
client: client,
|
||||
logger: Self.logger,
|
||||
alwaysReconnect: false
|
||||
)
|
||||
defer { manager.shutdown() }
|
||||
let connectionStream1 = MQTTConnectionStream(client: client, logger: Self.logger)
|
||||
let connectionStream2 = MQTTConnectionStream(client: client, logger: Self.logger)
|
||||
var events1 = [MQTTManager.Event]()
|
||||
var events2 = [MQTTManager.Event]()
|
||||
|
||||
let stream1 = connectionStream1.start()
|
||||
let stream2 = connectionStream2.start()
|
||||
|
||||
_ = try await manager.connect()
|
||||
|
||||
Task {
|
||||
while !client.isActive() {
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
}
|
||||
try await Task.sleep(for: .milliseconds(200))
|
||||
try await client.disconnect()
|
||||
try await Task.sleep(for: .milliseconds(500))
|
||||
manager.shutdown()
|
||||
try await Task.sleep(for: .milliseconds(500))
|
||||
connectionStream1.stop()
|
||||
connectionStream2.stop()
|
||||
}
|
||||
|
||||
for await event in stream1.removeDuplicates() {
|
||||
events1.append(event)
|
||||
}
|
||||
for await event in stream2.removeDuplicates() {
|
||||
events2.append(event)
|
||||
}
|
||||
|
||||
XCTAssertEqual(events1, [.disconnected, .connected, .disconnected, .shuttingDown])
|
||||
XCTAssertEqual(events2, [.disconnected, .connected, .disconnected, .shuttingDown])
|
||||
}
|
||||
|
||||
func testListeningResumesAfterDisconnectThenReconnect() async throws {
|
||||
struct TimeoutError: Error {}
|
||||
|
||||
@@ -99,6 +170,8 @@ final class SensorsClientTests: XCTestCase {
|
||||
userName: nil,
|
||||
password: nil
|
||||
)
|
||||
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
// return .init(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: Self.logger)
|
||||
let config = MQTTClient.Configuration(
|
||||
version: .v3_1_1,
|
||||
userName: envVars.userName,
|
||||
@@ -111,15 +184,14 @@ final class SensorsClientTests: XCTestCase {
|
||||
return .init(
|
||||
host: Self.hostname,
|
||||
identifier: identifier,
|
||||
eventLoopGroupProvider: .shared(MultiThreadedEventLoopGroup(numberOfThreads: 1)),
|
||||
eventLoopGroupProvider: .shared(eventLoopGroup),
|
||||
logger: Self.logger,
|
||||
configuration: config
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Helpers for tests.
|
||||
|
||||
// - MARK: Helpers
|
||||
struct TopicNotFoundError: Error {}
|
||||
|
||||
actor ResultContainer: Sendable {
|
||||
@@ -1,93 +0,0 @@
|
||||
import AsyncAlgorithms
|
||||
import Logging
|
||||
import Models
|
||||
import MQTTConnectionService
|
||||
@_spi(Internal) import MQTTManager
|
||||
import MQTTNIO
|
||||
import NIO
|
||||
import ServiceLifecycle
|
||||
import ServiceLifecycleTestKit
|
||||
import XCTest
|
||||
|
||||
final class MQTTConnectionServiceTests: XCTestCase {
|
||||
|
||||
static let hostname = ProcessInfo.processInfo.environment["MOSQUITTO_SERVER"] ?? "localhost"
|
||||
|
||||
static let logger: Logger = {
|
||||
var logger = Logger(label: "MQTTConnectionServiceTests")
|
||||
logger.logLevel = .trace
|
||||
return logger
|
||||
}()
|
||||
|
||||
// TODO: Move to integration tests.
|
||||
func testMQTTConnectionStream() async throws {
|
||||
let client = createClient(identifier: "testNonManagedStream")
|
||||
let manager = MQTTManager.live(
|
||||
client: client,
|
||||
logger: Self.logger,
|
||||
alwaysReconnect: false
|
||||
)
|
||||
defer { manager.shutdown() }
|
||||
let connectionStream1 = MQTTConnectionStream(client: client, logger: Self.logger)
|
||||
let connectionStream2 = MQTTConnectionStream(client: client, logger: Self.logger)
|
||||
var events1 = [MQTTManager.Event]()
|
||||
var events2 = [MQTTManager.Event]()
|
||||
|
||||
let stream1 = connectionStream1.start()
|
||||
let stream2 = connectionStream2.start()
|
||||
|
||||
_ = try await manager.connect()
|
||||
|
||||
Task {
|
||||
while !client.isActive() {
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
}
|
||||
try await Task.sleep(for: .milliseconds(200))
|
||||
manager.shutdown()
|
||||
try await client.disconnect()
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
try await client.shutdown()
|
||||
try await Task.sleep(for: .seconds(1))
|
||||
connectionStream1.stop()
|
||||
connectionStream2.stop()
|
||||
}
|
||||
|
||||
for await event in stream1.removeDuplicates() {
|
||||
events1.append(event)
|
||||
}
|
||||
for await event in stream2.removeDuplicates() {
|
||||
events2.append(event)
|
||||
}
|
||||
|
||||
XCTAssertEqual(events1, [.disconnected, .connected, .disconnected, .shuttingDown])
|
||||
XCTAssertEqual(events2, [.disconnected, .connected, .disconnected, .shuttingDown])
|
||||
}
|
||||
|
||||
func createClient(identifier: String) -> MQTTClient {
|
||||
let envVars = EnvVars(
|
||||
appEnv: .testing,
|
||||
host: Self.hostname,
|
||||
port: "1883",
|
||||
identifier: identifier,
|
||||
userName: nil,
|
||||
password: nil
|
||||
)
|
||||
let config = MQTTClient.Configuration(
|
||||
version: .v3_1_1,
|
||||
userName: envVars.userName,
|
||||
password: envVars.password,
|
||||
useSSL: false,
|
||||
useWebSockets: false,
|
||||
tlsConfiguration: nil,
|
||||
webSocketURLPath: nil
|
||||
)
|
||||
return .init(
|
||||
host: Self.hostname,
|
||||
identifier: identifier,
|
||||
eventLoopGroupProvider: .shared(MultiThreadedEventLoopGroup(numberOfThreads: 1)),
|
||||
logger: Self.logger,
|
||||
configuration: config
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
# Used this to build the release version of the image.
|
||||
# Build the executable
|
||||
FROM swift:5.10 AS build
|
||||
ARG SWIFT_IMAGE_VERSION="5.10"
|
||||
|
||||
FROM swift:${SWIFT_IMAGE_VERSION} AS build
|
||||
WORKDIR /build
|
||||
COPY ./Package.* ./
|
||||
RUN swift package resolve
|
||||
@@ -8,7 +10,6 @@ COPY . .
|
||||
RUN swift build -c release -Xswiftc -g
|
||||
|
||||
# Run image
|
||||
FROM swift:5.10-slim
|
||||
WORKDIR /run
|
||||
COPY --from=build /build/.build/release/dewpoint-controller /run
|
||||
CMD ["/bin/bash", "-xc", "./dewpoint-controller"]
|
||||
FROM swift:${SWIFT_IMAGE_VERSION}-slim
|
||||
COPY --from=build /build/.build/release/dewpoint-controller /usr/local/bin
|
||||
CMD ["/bin/bash", "-xc", "/usr/local/bin/dewpoint-controller run"]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# Used to build a test image.
|
||||
FROM swift:5.10
|
||||
ARG SWIFT_IMAGE_VERSION="5.10"
|
||||
FROM swift:${SWIFT_IMAGE_VERSION}
|
||||
WORKDIR /app
|
||||
COPY ./Package.* ./
|
||||
RUN swift package resolve
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# run this with docker-compose run test
|
||||
name: swift-mqtt-dewpoint-test
|
||||
|
||||
services:
|
||||
test:
|
||||
build:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
# run this with docker-compose run dewpoint_controller
|
||||
name: swift-mqtt-dewpoint
|
||||
|
||||
services:
|
||||
dewpoint_controller:
|
||||
container_name: dewpoint-controller
|
||||
|
||||
Reference in New Issue
Block a user