Skip to content

Commit

Permalink
Refactor the humidity input
Browse files Browse the repository at this point in the history
It now uses fewer timeouts, and somewhat simpler logic for deciding the
state of a room.
  • Loading branch information
yorickpeterse committed Sep 13, 2023
1 parent 875fb3c commit 676e939
Show file tree
Hide file tree
Showing 2 changed files with 53 additions and 85 deletions.
83 changes: 39 additions & 44 deletions src/openflow/inputs/humidity.inko
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,18 @@ class enum Humidity {
# The room is dry, no ventilation is necessary.
case Dry

# The humidity is increasing, and we may need to ventilate the room.
case Increasing
# The room is drying up.
case Drying

# The room is humid, and ventilation must be applied.
#
# The wrapped value is the time at which the room was marked as humid.
case Humid(Instant)
case Humid

fn humid? -> Bool {
match self {
case Humid -> true
case _ -> false
}
}
}

class pub Sensor {
Expand Down Expand Up @@ -66,13 +71,6 @@ class pub async Input {
# The value to add to the raw sensor values to obtain the correct value.
let @correction: Int

# The minimum amount of time to ventilate for, regardless of what the current
# humidity value is.
let @minimum_time: Duration

# The maximum amount of time to ventilate for.
let @maximum_time: Duration

# The amount of time humidity must be below the "low" threshold before
# ventilation can be disabled.
let @low_time: Duration
Expand All @@ -96,10 +94,8 @@ class pub async Input {
@high = config.high,
@low = config.low,
@correction = config.correction,
@minimum_time = recover Duration.from_secs(2700),
@low_time = recover Duration.from_secs(600),
@low_time = recover Duration.from_secs(1800),
@interval = recover Duration.from_secs(30),
@maximum_time = recover Duration.from_secs(5400),
}
}

Expand Down Expand Up @@ -140,8 +136,8 @@ class pub async Input {
update_sensor(sensor, humidity)

let humid = match sensor.humidity {
case Humid(_) -> true
case _ -> false
case Humid or Drying -> true
case Dry -> false
}

map.set(sensor.name, humid)
Expand All @@ -152,33 +148,32 @@ class pub async Input {
}

fn mut update_sensor(sensor: mut Sensor, humidity: Int) {
if humidity >= @high {
if humidity >= @high {
match sensor.humidity {
case Dry -> sensor.update(Humidity.Increasing)
case Increasing -> sensor.update(Humidity.Humid(Instant.new))
case Humid(_) -> sensor.update_time
}

return
}

let dry = match sensor.humidity {
case Humid(at) if at.elapsed >= @maximum_time -> {
info(sensor.name, 'maximum ventilation time reached')
true
case Humid -> sensor.update_time
case _ -> sensor.update(Humidity.Humid)
}
case Humid(_) if humidity > @low -> {
sensor.update_time
false
}
case Humid(at) -> {
at.elapsed >= @minimum_time and sensor.last_update.elapsed >= @low_time
} else {
match sensor.humidity {
case Humid if humidity <= @low -> sensor.update(Humidity.Drying)
case Humid if humidity > @low -> sensor.update_time
# When ventilation kicks in, the humidity drops, but after a little
# while increases again. This check ensures that we don't disable
# ventilation prematurely just because the reached the low target for a
# brief period of time.
#
# The extra "padding" is to account for sporadic spikes of 1%, which can
# sometimes happen due to rounding or just coincidence.
case Drying if humidity >= (@low + (@high - @low / 2)) -> {
sensor.update(Humidity.Humid)
}
case Drying if sensor.last_update.elapsed >= @low_time -> {
info(sensor.name, 'the room dried up, disabling ventilation')
sensor.update(Humidity.Dry)
}
case _ -> {}
}
case Increasing -> true
case Dry -> false
}

if dry { sensor.update(Humidity.Dry) }
}

fn apply(updates: uni Map[String, Bool]) {
Expand All @@ -187,10 +182,10 @@ class pub async Input {
let humid = entry.value
let room = rooms.get_mut(entry.key)

match room.status {
case _ if humid -> room.update(recover Status.Humid)
case Humid if humid.false? -> room.update(recover Status.Default)
case _ -> {}
if humid {
room.update(recover Status.Humid)
} else if room.status.humid? {
room.update(recover Status.Default)
}

apply_now or humid
Expand Down
55 changes: 14 additions & 41 deletions test/openflow/inputs/test_humidity.inko
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ impl Input {
@low_time = Duration.from_secs(0)
}

fn async mut reset_minimum_time {
@minimum_time = Duration.from_secs(0)
}

fn async mut reset_maximum_time {
@maximum_time = Duration.from_secs(0)
}

fn async mut wait(waiter: uni Waiter) {
waiter.notify
}
Expand Down Expand Up @@ -79,14 +71,6 @@ fn pub tests(t: mut Tests) {
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Default)
}

t.test('No ventilation is applied the first time humidity increases') fn (t) {
let state = state(allow_api_calls)
let input = input(state, humidity: recover [82], correction: 0)

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

t.test('Ventilation is applied when a room is humid for too long') fn (t) {
let state = state(allow_api_calls)
let input = input(state, humidity: recover [82, 82], correction: 0)
Expand All @@ -106,57 +90,46 @@ fn pub tests(t: mut Tests) {
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Humid)
}

t.test('Ventilation persists when the minimum amount of time has not yet passed') fn (t) {
t.test('Ventilation continues when humidity is high, even when too much time has passed') fn (t) {
let state = state(allow_api_calls)
let input = input(state, humidity: recover [82, 83, 73], correction: 0)
let input = input(state, humidity: recover [85, 85, 85], correction: 0)

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

t.test('Ventilation is disabled after enough time has passed') fn (t) {
t.test('Ventilation stops when humidity is low enough and too much time has passed') fn (t) {
let state = state(allow_api_calls)
let input = input(state, humidity: recover [82, 83, 73], correction: 0)
let input = input(state, humidity: recover [85, 85, 60, 60], correction: 0)

run(input)
run(input)
input.reset_low_time
input.reset_minimum_time
run(input)
run(input) # Transitions the state to "drying"
run(input) # Transitions the state to "dry"
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Default)
}

t.test('Ventilation continues when humidity is high, even when too much time has passed') fn (t) {
let state = state(allow_api_calls)
let input = input(state, humidity: recover [85, 85, 85], correction: 0)

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

t.test('Ventilation stops when humidity is low enough and too much time has passed') fn (t) {
t.test('Applying a humidity correction when the humidity is high') fn (t) {
let state = state(allow_api_calls)
let input = input(state, humidity: recover [85, 85, 60], correction: 0)
let input = input(state, humidity: recover [80, 80], correction: -10)

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

t.test('Applying a humidity correction when the humidity is high') fn (t) {
t.test('Handling increases in humidity after the humidity goes below the low threshold') fn (t) {
let state = state(allow_api_calls)
let input = input(state, humidity: recover [80, 80], correction: -10)
let input = input(state, humidity: recover [85, 85, 60, 77], correction: 0)

run(input)
run(input)
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Default)
input.reset_low_time
run(input) # Transitions the state to "drying"
run(input) # Transitions the state to "humid" again
t.equal(Snapshot.of(state).rooms.get('bathroom').status, Status.Humid)
}
}

0 comments on commit 676e939

Please sign in to comment.