Skip to main content

Thermostat

A rudimentary thermostat controller that uses a temperature sensor to decide when to turn on or off a furnace controlled by a relay.

Logging sensor data

Let's start by mounting a temperature service client and logging each sensor reading to the console (using console.data).

import { Temperature } from "@devicescript/core"

const thermometer = new Temperature()
thermometer.reading.subscribe(t => {
console.data({ t })
})

In Visual Studio Code, you can run this program with a simulated device and sensor and collect virtual data. Click on the Download Data icon in the DeviceScript view, you can analyze the data in a notebook.

This approach works for a basic scenario but we lack the control over when data arrives, how it is filtered and at which rate. This is where observables come into play.

Add observables

Add this import to your main.ts file (the @devicescript/observables is builtin).

import "@devicescript/observables"

Filtering

Observables provide a way to add operators over streams of data. A register like temperature is like a stream of readings and we'll use operators to manipulate them.

We start with the ewma operator, which applies a exponentially weighted moving average filter to the data.

import { Temperature } from "@devicescript/core"
import { ewma } from "@devicescript/observables"

const thermometer = new Temperature()
thermometer.reading
.pipe(ewma(0.9))
.subscribe(t => console.data({ t }))

Tapping

import { Temperature } from "@devicescript/core"
import { ewma, tap } from "@devicescript/observables"

const thermometer = new Temperature()
thermometer.reading
.pipe(
tap(t_raw => console.data({ t_raw })),
ewma(0.9)
)
.subscribe(t => console.data({ t }))

Throttling

Although the sensor may produce a high frequency of data locally, we probably want to throttle the output to a slower pace when deciding to control the relay. This can be done through throttleTime (stream first value and wait) or auditTime (wait then stream last value).

import { Temperature } from "@devicescript/core"
import { ewma, tap, auditTime } from "@devicescript/observables"

const thermometer = new Temperature()
thermometer.reading
.pipe(
tap(t_raw => console.data({ t_raw })),
ewma(0.9),
tap(t_ewma => console.data({ t_ewma })),
auditTime(5000) // once every 5 seconds
)
.subscribe(t => console.data({ t }))

Level detector

The next step is to categorize the current temperature in 3 zones, or levels: low, mid, high. In the low zone, the relay should be turn on to heat the room. In the high zone, the relay should be turned off. In the mid zone, the relay should not be actuated to avoid switching at the boundary of the levels.

import { Temperature } from "@devicescript/core"
import { ewma, tap, auditTime, levelDetector } from "@devicescript/observables"

const thermometer = new Temperature()
const t_ref = 68 // degree F

thermometer.reading
.pipe(
tap(t_raw => console.data({ t_raw })),
ewma(0.9),
tap(t_ewma => console.data({ t_ewma })),
auditTime(5000), // once every 5 seconds
tap(t_audit => console.data({ t_audit })),
levelDetector(t_ref - 1, t_ref + 1), // -1 = low, 0 = mid, 1 = high
tap(level => console.data({ level }))
)
.subscribe(level => console.data({ level }))

Relay

import { Temperature, Relay } from "@devicescript/core"
import { ewma, tap, auditTime, levelDetector } from "@devicescript/observables"

const thermometer = new Temperature()
const t_ref = 68 // degree F
const relay = new Relay()

thermometer.reading
.pipe(
tap(t_raw => console.data({ t_raw })),
ewma(0.9),
tap(t_ewma => console.data({ t_ewma })),
auditTime(5000),
tap(t_audit => console.data({ t_audit })),
levelDetector(t_ref - 1, t_ref + 1),
tap(level => console.data({ level }))
)
.subscribe(async level => {
if (level < 0) await relay.enabled.write(true)
else if (level > 0) await relay.enabled.write(false)
console.data({ relay: await relay.enabled.read() })
})

Relay on ESP32

Using a ESP32 board and a relay on pin A0, we can finalize this example.

import { pins } from "@dsboard/adafruit_qt_py_c3"
import { Temperature } from "@devicescript/core"
import { ewma, tap, auditTime, levelDetector } from "@devicescript/observables"
import { startRelay } from "@devicescript/servers"

const thermometer = new Temperature()
const t_ref = 68 // degree F
const relay = startRelay({
pin: pins.A0_D0,
})

thermometer.reading
.pipe(
tap(t_raw => console.data({ t_raw })),
ewma(0.9),
tap(t_ewma => console.data({ t_ewma })),
auditTime(5000),
tap(t_audit => console.data({ t_audit })),
levelDetector(t_ref - 1, t_ref + 1),
tap(level => console.data({ level }))
)
.subscribe(async level => {
if (level < 0) await relay.enabled.write(true)
else if (level > 0) await relay.enabled.write(false)
console.data({ relay: await relay.enabled.read() })
})