Skip to content

Commit

Permalink
Handle humidity increases below the threshold
Browse files Browse the repository at this point in the history
When the indoor air is dry (e.g. during the summer) and one takes a
shower, the humidity might increase but not enough to cross the absolute
threshold.

To solve this, the humidity input maintains a ring buffer of humidity
samples. When the most recent sample is greater than a certain
percentage (15% by default) compared to 5 minutes ago, ventilation is
enabled regardless of the absolute humidity. So if the humidity
increases from 55% to 70%, but the threshold is 80%, humidity is still
enabled.

This increase acts as an OR with the absolute threshold, ensuring that
if humidity slowly increases we still enable ventilation when necessary.
  • Loading branch information
yorickpeterse committed May 15, 2024
1 parent 2876892 commit b8ab736
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 11 deletions.
14 changes: 13 additions & 1 deletion src/openflow/config.inko
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,10 @@ class pub Humidity {
# The threshold at which to return to normal ventilation.
let pub @low: Int

# If humidity increases by this value then ventilation is enabled, regardless
# of the absolute value.
let pub @max_increase: Int

# The value to add to the raw sensor values to obtain the correct value.
#
# The Itho humidity sensors appear to not be entirely accurate, sometimes
Expand All @@ -305,9 +309,17 @@ class pub Humidity {
) -> Result[Humidity, String] {
let high = try int(object, 'high')
let low = try int(object, 'low')
let max_increase = opt_int(object, 'max_increase').or(15)
let correct = opt_int(object, 'correction').or(-5)

Result.Ok(Humidity(high: high, low: low, correction: correct))
Result.Ok(
Humidity(
high: high,
low: low,
max_increase: max_increase,
correction: correct,
),
)
}
}

Expand Down
63 changes: 55 additions & 8 deletions src/openflow/inputs/humidity.inko
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,30 @@ import openflow.state (State, Status)
import std.process (sleep)
import std.time (Duration, Instant)

# The amount of time between checking the sensor status.
let CHECK_INTERVAL = 30

# The amount of time to maintain humidity samples for.
let SAMPLE_INTERVAL = 300

# The number of humidity samples to maintain.
let HUMIDITY_SAMPLES = 300 / 30

# A ring buffer of humidity samples.
class pub Samples {
let @values: Array[Int]
let @index: Int

fn pub static new(capacity: Int) -> Samples {
Samples(values: Array.filled(capacity, with: 0), index: 0)
}

# Pushes a new value into the buffer, returning the oldest value.
fn pub mut push(value: Int) -> Int {
@values.swap(@index := (@index + 1 % @values.size), value)
}
}

# The state of a single humidity sensor.
class enum Humidity {
# The room is dry, no ventilation is necessary.
Expand All @@ -29,10 +53,17 @@ class pub Sensor {
let @name: String
let @id: String
let @humidity: Humidity
let @last_update: Instant
let @samples: Samples
let @last_status_update: Instant

fn pub static new(name: String, id: String) -> Sensor {
Sensor(name: name, id: id, humidity: Humidity.Dry, last_update: Instant.new)
Sensor(
name: name,
id: id,
humidity: Humidity.Dry,
samples: Samples.new(HUMIDITY_SAMPLES),
last_status_update: Instant.new,
)
}

fn mut update(humidity: Humidity) {
Expand All @@ -41,7 +72,7 @@ class pub Sensor {
}

fn mut update_time {
@last_update = Instant.new
@last_status_update = Instant.new
}
}

Expand All @@ -63,6 +94,10 @@ class pub async Input {
# enough.
let @low: Int

# If humidity increases by this value then ventilation is enabled, regardless
# of the absolute value.
let @max_increase: Int

# The value to add to the raw sensor values to obtain the correct value.
let @correction: Int

Expand All @@ -71,7 +106,7 @@ class pub async Input {
let @low_time: Duration

# The interval at which to check the humidity sensors.
let @interval: Duration
let @check_interval: Duration

fn pub static new(
state: State,
Expand All @@ -88,9 +123,10 @@ class pub async Input {
sensors: recover [],
high: config.high,
low: config.low,
max_increase: config.max_increase,
correction: config.correction,
low_time: recover Duration.from_secs(1800),
interval: recover Duration.from_secs(30),
check_interval: recover Duration.from_secs(CHECK_INTERVAL),
)
}

Expand All @@ -101,7 +137,7 @@ class pub async Input {
fn pub async mut run {
loop {
iteration
sleep(@interval)
sleep(@check_interval)
}
}

Expand Down Expand Up @@ -147,7 +183,18 @@ class pub async Input {
}

fn mut update_sensor(sensor: mut Sensor, humidity: Int) {
if humidity >= @high {
let large_increase = match sensor.samples.push(humidity) {
case n if n > 0 and humidity - n >= @max_increase -> {
info(
sensor.name,
'humidity increased by more than ${@max_increase}%, enabling ventilation',
)
true
}
case _ -> false
}

if humidity >= @high or large_increase {
match sensor.humidity {
case Humid -> sensor.update_time
case _ -> sensor.update(Humidity.Humid)
Expand All @@ -166,7 +213,7 @@ class pub async Input {
case Drying if humidity >= (@low + (@high - @low / 2)) -> {
sensor.update(Humidity.Humid)
}
case Drying if sensor.last_update.elapsed >= @low_time -> {
case Drying if sensor.last_status_update.elapsed >= @low_time -> {
info(sensor.name, 'the room dried up, disabling ventilation')
sensor.update(Humidity.Dry)
}
Expand Down
64 changes: 62 additions & 2 deletions test/openflow/inputs/test_humidity.inko
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import helpers (NullWriter, Snapshot, allow_api_calls, state)
import openflow.config (Humidity)
import openflow.http (Client, Driver, Response, TestDriver)
import openflow.inputs.humidity (Input, Sensor)
import openflow.inputs.humidity (Input, Samples, Sensor)
import openflow.itho (Itho)
import openflow.logger (Logger)
import openflow.metrics (Metrics)
Expand Down Expand Up @@ -48,7 +48,12 @@ fn input(state: State, humidity: uni Array[Int], correction: Int) -> Input {
)

let itho = recover Itho.new(Client.with_driver(driver as Driver))
let conf = Humidity(high: 80, low: 75, correction: correction)
let conf = Humidity(
high: 80,
low: 75,
max_increase: 15,
correction: correction,
)
let input = Input.new(state, logger, metrics, itho, conf)

input.add_sensor(recover Sensor.new('bathroom', id: 'RH bathroom 1'))
Expand All @@ -64,6 +69,31 @@ fn run(input: Input) {
}

fn pub tests(t: mut Tests) {
t.test('Samples.new', fn (t) {
let buf = Samples.new(1)

t.equal(buf.index, 0)
t.equal(buf.values.size, 1)
})

t.test('Samples.push', fn (t) {
let buf = Samples.new(2)

t.equal(buf.push(10), 0)
t.equal(buf.index, 1)

t.equal(buf.push(20), 0)
t.equal(buf.index, 0)

t.equal(buf.values.get(0), 10)
t.equal(buf.values.get(1), 20)

t.equal(buf.push(30), 10)
t.equal(buf.push(40), 20)
t.equal(buf.values.get(0), 30)
t.equal(buf.values.get(1), 40)
})

t.test('No ventilation is applied when the humidity is OK', fn (t) {
let state = state(allow_api_calls)
let input = input(state, humidity: recover [70], correction: 0)
Expand Down Expand Up @@ -150,4 +180,34 @@ fn pub tests(t: mut Tests) {
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Humid)
},
)

t.test(
'Ventilation is applied when the humidity increase is too big',
fn (t) {
let state = state(allow_api_calls)
let input = input(
state,
humidity: recover {
[
55, 55, 55, 55, 55, 55, 55, 55, 55, 55, 70, 70, 70, 70, 70, 70, 70,
70, 70, 70, 70, 70, 30, 0,
]
},
correction: 0,
)

11.times(fn (_) { run(input) })
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Humid)

11.times(fn (_) { run(input) })
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Humid)

input.reset_low_time
run(input)
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Default)

run(input)
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Default)
},
)
}

0 comments on commit b8ab736

Please sign in to comment.