feat: Renaming and moves some items around, listeners now manage reconnection events.
All checks were successful
CI / Run Tests (push) Successful in 4m16s
All checks were successful
CI / Run Tests (push) Successful in 4m16s
This commit is contained in:
@@ -1,42 +0,0 @@
|
||||
import NIO
|
||||
import NIOFoundationCompat
|
||||
import PsychrometricClient
|
||||
|
||||
/// Represents a type that can be initialized by a ``ByteBuffer``.
|
||||
protocol BufferInitalizable {
|
||||
init?(buffer: inout ByteBuffer)
|
||||
}
|
||||
|
||||
extension Double: BufferInitalizable {
|
||||
|
||||
/// Attempt to create / parse a double from a byte buffer.
|
||||
init?(buffer: inout ByteBuffer) {
|
||||
guard let string = buffer.readString(
|
||||
length: buffer.readableBytes,
|
||||
encoding: String.Encoding.utf8
|
||||
)
|
||||
else { return nil }
|
||||
self.init(string)
|
||||
}
|
||||
}
|
||||
|
||||
extension Tagged: BufferInitalizable where RawValue: BufferInitalizable {
|
||||
init?(buffer: inout ByteBuffer) {
|
||||
guard let value = RawValue(buffer: &buffer) else { return nil }
|
||||
self.init(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Humidity<Relative>: BufferInitalizable {
|
||||
init?(buffer: inout ByteBuffer) {
|
||||
guard let value = Double(buffer: &buffer) else { return nil }
|
||||
self.init(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Temperature<DryAir>: BufferInitalizable {
|
||||
init?(buffer: inout ByteBuffer) {
|
||||
guard let value = Double(buffer: &buffer) else { return nil }
|
||||
self.init(value)
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,11 @@ import DependenciesMacros
|
||||
import Foundation
|
||||
import Logging
|
||||
import Models
|
||||
import MQTTConnectionManager
|
||||
import MQTTManager
|
||||
import MQTTNIO
|
||||
import NIO
|
||||
import PsychrometricClient
|
||||
import ServiceLifecycle
|
||||
import TopicDependencies
|
||||
|
||||
/// Service that is responsible for listening to changes of the temperature and humidity
|
||||
/// sensors, then publishing back the calculated dew-point temperature and enthalpy for
|
||||
@@ -17,9 +16,7 @@ import TopicDependencies
|
||||
///
|
||||
public actor SensorsService: Service {
|
||||
|
||||
@Dependency(\.mqttConnectionManager.stream) var connectionStream
|
||||
@Dependency(\.topicListener) var topicListener
|
||||
@Dependency(\.topicPublisher) var topicPublisher
|
||||
@Dependency(\.mqtt) var mqtt
|
||||
|
||||
/// The logger to use for the service.
|
||||
private let logger: Logger?
|
||||
@@ -27,12 +24,9 @@ public actor SensorsService: Service {
|
||||
/// The sensors that we are listening for updates to, so
|
||||
/// that we can calculate the dew-point temperature and enthalpy
|
||||
/// values to publish back to the MQTT broker.
|
||||
var sensors: [TemperatureAndHumiditySensor]
|
||||
private var sensors: [TemperatureAndHumiditySensor]
|
||||
|
||||
@_spi(Internal)
|
||||
public var isListening: Bool = false
|
||||
|
||||
var topics: [String] {
|
||||
private var topics: [String] {
|
||||
sensors.reduce(into: [String]()) { array, sensor in
|
||||
array.append(sensor.topics.temperature)
|
||||
array.append(sensor.topics.humidity)
|
||||
@@ -60,33 +54,16 @@ public actor SensorsService: Service {
|
||||
public func run() async throws {
|
||||
precondition(sensors.count > 0, "Sensors should not be empty.")
|
||||
|
||||
try await withGracefulShutdownHandler {
|
||||
// Listen for connection events, so that we can automatically
|
||||
// reconnect any sensor topics we're listening to upon a disconnect / reconnect
|
||||
// event. We can also shutdown any topic listeners upon a shutdown event.
|
||||
for await event in try connectionStream().cancelOnGracefulShutdown() {
|
||||
switch event {
|
||||
case .shuttingDown:
|
||||
logger?.debug("Received shutdown event.")
|
||||
isListening = false
|
||||
try await self.shutdown()
|
||||
case .disconnected:
|
||||
logger?.debug("Received disconnected event.")
|
||||
isListening = false
|
||||
try await Task.sleep(for: .milliseconds(100))
|
||||
case .connected:
|
||||
logger?.debug("Received connected event.")
|
||||
let stream = try await makeStream()
|
||||
isListening = true
|
||||
for await result in stream.cancelOnGracefulShutdown() {
|
||||
logger?.debug("Received result for topic: \(result.topic)")
|
||||
await self.handleResult(result)
|
||||
}
|
||||
}
|
||||
let stream = try await makeStream()
|
||||
|
||||
await withGracefulShutdownHandler {
|
||||
for await result in stream.cancelOnGracefulShutdown() {
|
||||
logger?.debug("Received result for topic: \(result.topic)")
|
||||
await handleResult(result)
|
||||
}
|
||||
} onGracefulShutdown: {
|
||||
self.logger?.debug("Received graceful shutdown.")
|
||||
Task {
|
||||
self.logger?.debug("Received graceful shutdown.")
|
||||
try await self.shutdown()
|
||||
}
|
||||
}
|
||||
@@ -95,24 +72,13 @@ public actor SensorsService: Service {
|
||||
@_spi(Internal)
|
||||
public func shutdown() async throws {
|
||||
try await publishUpdates()
|
||||
topicListener.shutdown()
|
||||
}
|
||||
|
||||
private func makeStream() async throws -> AsyncStream<(buffer: ByteBuffer, topic: String)> {
|
||||
try await topicListener.listen(to: topics)
|
||||
// ignore errors, so that we continue to listen, but log them
|
||||
// for debugging purposes.
|
||||
.compactMap { result in
|
||||
switch result {
|
||||
case let .failure(error):
|
||||
self.logger?.debug("Received error listening for sensors: \(error)")
|
||||
return nil
|
||||
case let .success(info):
|
||||
return (info.payload, info.topicName)
|
||||
}
|
||||
}
|
||||
// ignore duplicate values, to prevent publishing dew-point and enthalpy
|
||||
// changes to frequently.
|
||||
// ignore duplicate values, to prevent publishing dew-point and enthalpy
|
||||
// changes to frequently.
|
||||
try await mqtt.listen(to: topics)
|
||||
.map { ($0.payload, $0.topicName) }
|
||||
.removeDuplicates { lhs, rhs in
|
||||
lhs.buffer == rhs.buffer
|
||||
&& lhs.topic == rhs.topic
|
||||
@@ -127,8 +93,7 @@ public actor SensorsService: Service {
|
||||
logger?.debug("Begin handling result for topic: \(topic)")
|
||||
|
||||
func decode<V: BufferInitalizable>(_: V.Type) -> V? {
|
||||
var buffer = result.buffer
|
||||
return V(buffer: &buffer)
|
||||
return V(buffer: result.buffer)
|
||||
}
|
||||
|
||||
if topic.contains("temperature") {
|
||||
@@ -159,9 +124,9 @@ public actor SensorsService: Service {
|
||||
|
||||
private func publish(_ double: Double?, to topic: String) async throws {
|
||||
guard let double else { return }
|
||||
try await topicPublisher.publish(
|
||||
try await mqtt.publish(
|
||||
ByteBufferAllocator().buffer(string: "\(double)"),
|
||||
to: topic,
|
||||
payload: ByteBufferAllocator().buffer(string: "\(double)"),
|
||||
qos: .exactlyOnce,
|
||||
retain: true
|
||||
)
|
||||
@@ -210,3 +175,38 @@ private extension Array where Element == TemperatureAndHumiditySensor {
|
||||
self[index].needsProcessed = false
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a type that can be initialized by a ``ByteBuffer``.
|
||||
protocol BufferInitalizable {
|
||||
init?(buffer: ByteBuffer)
|
||||
}
|
||||
|
||||
extension Double: BufferInitalizable {
|
||||
|
||||
/// Attempt to create / parse a double from a byte buffer.
|
||||
init?(buffer: ByteBuffer) {
|
||||
let string = String(buffer: buffer)
|
||||
self.init(string)
|
||||
}
|
||||
}
|
||||
|
||||
extension Tagged: BufferInitalizable where RawValue: BufferInitalizable {
|
||||
init?(buffer: ByteBuffer) {
|
||||
guard let value = RawValue(buffer: buffer) else { return nil }
|
||||
self.init(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Humidity<Relative>: BufferInitalizable {
|
||||
init?(buffer: ByteBuffer) {
|
||||
guard let value = Double(buffer: buffer) else { return nil }
|
||||
self.init(value)
|
||||
}
|
||||
}
|
||||
|
||||
extension Temperature<DryAir>: BufferInitalizable {
|
||||
init?(buffer: ByteBuffer) {
|
||||
guard let value = Double(buffer: buffer) else { return nil }
|
||||
self.init(value)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user