Initial Commit
This commit is contained in:
87
Sources/Bootstrap/Bootstrap.swift
Normal file
87
Sources/Bootstrap/Bootstrap.swift
Normal file
@@ -0,0 +1,87 @@
|
||||
import DewPointEnvironment
|
||||
import EnvVars
|
||||
import Logging
|
||||
import Foundation
|
||||
import MQTTNIO
|
||||
import NIO
|
||||
import RelayClient
|
||||
import TemperatureSensorClient
|
||||
|
||||
public func bootstrap(
|
||||
eventLoopGroup: EventLoopGroup,
|
||||
logger: Logger? = nil
|
||||
) -> EventLoopFuture<DewPointEnvironment> {
|
||||
logger?.debug("Bootstrapping Dew Point Controller...")
|
||||
return loadEnvVars(eventLoopGroup: eventLoopGroup, logger: logger)
|
||||
.map { (envVars) -> DewPointEnvironment in
|
||||
let mqttClient = MQTTClient(envVars: envVars, eventLoopGroup: eventLoopGroup, logger: logger)
|
||||
return DewPointEnvironment.init(
|
||||
mqttClient: mqttClient,
|
||||
envVars: envVars,
|
||||
relayClient: .live(client: mqttClient),
|
||||
temperatureSensorClient: .live(client: mqttClient)
|
||||
)
|
||||
}
|
||||
.flatMap { environment in
|
||||
environment.mqttClient.logger.debug("Connecting to MQTT broker...")
|
||||
return environment.mqttClient.connect()
|
||||
.map { _ in environment }
|
||||
}
|
||||
}
|
||||
|
||||
private func loadEnvVars(eventLoopGroup: EventLoopGroup, logger: Logger?) -> EventLoopFuture<EnvVars> {
|
||||
|
||||
logger?.debug("Loading env vars...")
|
||||
|
||||
let envFilePath = URL(fileURLWithPath: #file)
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent(".dewPoint-env")
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
let defaultEnvVars = EnvVars()
|
||||
|
||||
let defaultEnvDict = (try? encoder.encode(defaultEnvVars))
|
||||
.flatMap { try? decoder.decode([String: String].self, from: $0) }
|
||||
?? [:]
|
||||
|
||||
// Read from file `.dewPoint-env` file if it exists.
|
||||
let localEnvVarsDict = (try? Data(contentsOf: envFilePath))
|
||||
.flatMap { try? decoder.decode([String: String].self, from: $0) }
|
||||
?? [:]
|
||||
|
||||
// Merge with variables in the shell environment.
|
||||
let envVarsDict = defaultEnvDict
|
||||
.merging(localEnvVarsDict, uniquingKeysWith: { $1 })
|
||||
.merging(ProcessInfo.processInfo.environment, uniquingKeysWith: { $1 })
|
||||
|
||||
// Produces the final env vars from the merged items or uses defaults if something
|
||||
// went wrong.
|
||||
let envVars = (try? JSONSerialization.data(withJSONObject: envVarsDict))
|
||||
.flatMap { try? decoder.decode(EnvVars.self, from: $0) }
|
||||
?? defaultEnvVars
|
||||
|
||||
logger?.debug("Done loading env vars...")
|
||||
return eventLoopGroup.next().makeSucceededFuture(envVars)
|
||||
}
|
||||
|
||||
extension 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: .v5_0,
|
||||
userName: envVars.userName,
|
||||
password: envVars.password
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
24
Sources/DewPointEnvironment/Environment.swift
Normal file
24
Sources/DewPointEnvironment/Environment.swift
Normal file
@@ -0,0 +1,24 @@
|
||||
import EnvVars
|
||||
import MQTTNIO
|
||||
import RelayClient
|
||||
import TemperatureSensorClient
|
||||
|
||||
public struct DewPointEnvironment {
|
||||
|
||||
public var mqttClient: MQTTClient
|
||||
public var envVars: EnvVars
|
||||
public var relayClient: RelayClient
|
||||
public var temperatureSensorClient: TemperatureSensorClient
|
||||
|
||||
public init(
|
||||
mqttClient: MQTTClient,
|
||||
envVars: EnvVars,
|
||||
relayClient: RelayClient,
|
||||
temperatureSensorClient: TemperatureSensorClient
|
||||
) {
|
||||
self.mqttClient = mqttClient
|
||||
self.envVars = envVars
|
||||
self.relayClient = relayClient
|
||||
self.temperatureSensorClient = temperatureSensorClient
|
||||
}
|
||||
}
|
||||
43
Sources/EnvVars/EnvVars.swift
Normal file
43
Sources/EnvVars/EnvVars.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
import Foundation
|
||||
|
||||
public struct EnvVars: Codable, Equatable {
|
||||
|
||||
public var appEnv: AppEnv
|
||||
public var host: String
|
||||
public var port: String?
|
||||
public var identifier: String
|
||||
public var userName: String?
|
||||
public var password: String?
|
||||
|
||||
public init(
|
||||
appEnv: AppEnv = .development,
|
||||
host: String = "127.0.0.1",
|
||||
port: String? = "1883",
|
||||
identifier: String = "dewPoint-controller",
|
||||
userName: String? = "mqtt_user",
|
||||
password: String? = "secret!"
|
||||
){
|
||||
self.appEnv = appEnv
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.identifier = identifier
|
||||
self.userName = userName
|
||||
self.password = password
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case appEnv = "APP_ENV"
|
||||
case host = "MQTT_HOST"
|
||||
case port = "MQTT_PORT"
|
||||
case identifier = "MQTT_IDENTIFIER"
|
||||
case userName = "MQTT_USERNAME"
|
||||
case password = "MQTT_PASSWORD"
|
||||
}
|
||||
|
||||
public enum AppEnv: String, Codable {
|
||||
case development
|
||||
case production
|
||||
case staging
|
||||
case testing
|
||||
}
|
||||
}
|
||||
8
Sources/Models/HumiditySensor.swift
Normal file
8
Sources/Models/HumiditySensor.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
public struct HumiditySensor: Equatable {
|
||||
public var topic: String
|
||||
|
||||
public init(topic: String) {
|
||||
self.topic = topic
|
||||
}
|
||||
}
|
||||
8
Sources/Models/Relay.swift
Normal file
8
Sources/Models/Relay.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
public struct Relay {
|
||||
public var topic: String
|
||||
|
||||
public init(topic: String) {
|
||||
self.topic = topic
|
||||
}
|
||||
}
|
||||
8
Sources/Models/TemperatureSensor.swift
Normal file
8
Sources/Models/TemperatureSensor.swift
Normal file
@@ -0,0 +1,8 @@
|
||||
|
||||
public struct TemperatureSensor: Equatable {
|
||||
public var topic: String
|
||||
|
||||
public init(topic: String) {
|
||||
self.topic = topic
|
||||
}
|
||||
}
|
||||
20
Sources/RelayClient/Interface.swift
Normal file
20
Sources/RelayClient/Interface.swift
Normal file
@@ -0,0 +1,20 @@
|
||||
import Foundation
|
||||
import Models
|
||||
import NIO
|
||||
|
||||
public struct RelayClient {
|
||||
public var toggle: (Relay) -> EventLoopFuture<Void>
|
||||
public var turnOn: (Relay) -> EventLoopFuture<Void>
|
||||
public var turnOff: (Relay) -> EventLoopFuture<Void>
|
||||
|
||||
public init(
|
||||
toggle: @escaping (Relay) -> EventLoopFuture<Void>,
|
||||
turnOn: @escaping (Relay) -> EventLoopFuture<Void>,
|
||||
turnOff: @escaping (Relay) -> EventLoopFuture<Void>
|
||||
) {
|
||||
self.toggle = toggle
|
||||
self.turnOn = turnOn
|
||||
self.turnOff = turnOff
|
||||
}
|
||||
}
|
||||
|
||||
37
Sources/RelayClient/Live.swift
Normal file
37
Sources/RelayClient/Live.swift
Normal file
@@ -0,0 +1,37 @@
|
||||
import Models
|
||||
import MQTTNIO
|
||||
import NIO
|
||||
|
||||
extension RelayClient {
|
||||
|
||||
public static func live(client: MQTTClient) -> RelayClient {
|
||||
.init(
|
||||
toggle: { relay in
|
||||
client.publish(relay: relay, state: .toggle)
|
||||
},
|
||||
turnOn: { relay in
|
||||
client.publish(relay: relay, state: .on)
|
||||
},
|
||||
turnOff: { relay in
|
||||
client.publish(relay: relay, state: .off)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Relay {
|
||||
enum State: String {
|
||||
case toggle, on, off
|
||||
}
|
||||
}
|
||||
|
||||
extension MQTTClient {
|
||||
|
||||
func publish(relay: Relay, state: Relay.State, qos: MQTTQoS = .atLeastOnce) -> EventLoopFuture<Void> {
|
||||
publish(
|
||||
to: relay.topic,
|
||||
payload: ByteBufferAllocator().buffer(string: state.rawValue),
|
||||
qos: qos
|
||||
)
|
||||
}
|
||||
}
|
||||
14
Sources/TemperatureSensorClient/Interface.swift
Normal file
14
Sources/TemperatureSensorClient/Interface.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
import Foundation
|
||||
import CoreUnitTypes
|
||||
import Models
|
||||
import NIO
|
||||
|
||||
public struct TemperatureSensorClient {
|
||||
public var state: (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>
|
||||
|
||||
public init(
|
||||
state: @escaping (TemperatureSensor, PsychrometricEnvironment.Units?) -> EventLoopFuture<Temperature>
|
||||
) {
|
||||
self.state = state
|
||||
}
|
||||
}
|
||||
51
Sources/TemperatureSensorClient/Live.swift
Normal file
51
Sources/TemperatureSensorClient/Live.swift
Normal file
@@ -0,0 +1,51 @@
|
||||
import CoreUnitTypes
|
||||
import Foundation
|
||||
import MQTTNIO
|
||||
|
||||
extension TemperatureSensorClient {
|
||||
|
||||
public static func live(client: MQTTClient) -> TemperatureSensorClient {
|
||||
.init(
|
||||
state: { sensor, units in
|
||||
client.logger.debug("Adding listener for temperature sensor...")
|
||||
let subscription = MQTTSubscribeInfoV5.init(topicFilter: sensor.topic, qos: .atLeastOnce)
|
||||
return client.v5.subscribe(to: [subscription])
|
||||
.flatMap { _ in
|
||||
let promise = client.eventLoopGroup.next().makePromise(of: Temperature.self)
|
||||
client.addPublishListener(named: "temperature-sensor", { result in
|
||||
switch result.temperature() {
|
||||
case let .success(celsius):
|
||||
let userUnits = units ?? PsychrometricEnvironment.shared.units
|
||||
let temperatureUnits = Temperature.Units.defaultFor(units: userUnits)
|
||||
promise.succeed(.init(celsius[temperatureUnits], units: temperatureUnits))
|
||||
case let .failure(error):
|
||||
promise.fail(error)
|
||||
}
|
||||
})
|
||||
|
||||
return promise.futureResult
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public enum TemperatureError: Error {
|
||||
case invalidTemperature
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
extension Result where Success == MQTTPublishInfo, Failure == Error {
|
||||
|
||||
fileprivate func temperature() -> Result<Temperature, Error> {
|
||||
flatMap { info in
|
||||
var buffer = info.payload
|
||||
guard let string = buffer.readString(length: buffer.readableBytes),
|
||||
let temperatureValue = Double(string)
|
||||
else {
|
||||
return .failure(TemperatureError.invalidTemperature)
|
||||
}
|
||||
return .success(.celsius(temperatureValue))
|
||||
}
|
||||
}
|
||||
}
|
||||
34
Sources/dewPoint-controller/main.swift
Normal file
34
Sources/dewPoint-controller/main.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
import Bootstrap
|
||||
import Logging
|
||||
import Models
|
||||
import MQTTNIO
|
||||
import NIO
|
||||
import RelayClient
|
||||
import Foundation
|
||||
|
||||
var logger = Logger(label: "dewPoint-logger")
|
||||
logger.logLevel = .debug
|
||||
logger.debug("Swift Dew Point Controller!")
|
||||
|
||||
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
let environment = try bootstrap(eventLoopGroup: eventLoopGroup, logger: logger).wait()
|
||||
let relayClient = environment.relayClient
|
||||
let relay = Relay(topic: "frankensystem/relays/switch/relay_1/command")
|
||||
let tempSensor = TemperatureSensor(topic: "frankensystem/relays/sensor/temperature_-_1/state")
|
||||
|
||||
defer {
|
||||
logger.debug("Disconnecting")
|
||||
_ = try? environment.mqttClient.disconnect().wait()
|
||||
try? environment.mqttClient.syncShutdownGracefully()
|
||||
}
|
||||
|
||||
while true {
|
||||
logger.debug("Toggling relay.")
|
||||
_ = try relayClient.toggle(relay).wait()
|
||||
|
||||
logger.debug("Reading temperature sensor.")
|
||||
let temp = try environment.temperatureSensorClient.state(tempSensor, .imperial).wait()
|
||||
logger.debug("Temperature: \(temp)")
|
||||
|
||||
Thread.sleep(forTimeInterval: 5)
|
||||
}
|
||||
Reference in New Issue
Block a user