thermostat-pi-dht
Version:
A Node.js framework (standalone app + lib) to control a heating system with a Raspberry Pi using DHT11 or DHT22/AM2302 sensors and GPIO actuators.
297 lines • 12.6 kB
JavaScript
"use strict";
// Project: thermostat-pi-dht
// File: Thermostat.ts
//
// Copyright 2021 Henning Kerstan
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Thermostat = void 0;
const delay_1 = __importDefault(require("delay"));
const pigpio_1 = require("pigpio");
const pigpio_dht_1 = __importDefault(require("pigpio-dht"));
/** An implementation of a thermostat on a Raspberry Pi using
* - a DHT11 or DHT22/AM2302 sensor and
* - a GPIO as actuator.
*
* Once activated, a thermostat regularly measures the current [[temperature]] (and relative [[humidity]]). It then activates or deactivates the [[actuatorPin | actuator GPIO]] (which in turn should control the heating, e.g. by using a relay) by comparing the current [[temperature]] with the given [[setpoint]]:
* - if the [[temperature]] is below the [[setpoint]], it activates the actuator and
* - if the [[temperature]] is equal to or above the [[setpoint]], it deactivates the actuator.
*
* ## Common measuring loop
* Note that this implementation uses a *common measuring loop* for all configured thermostats, hence the [[samplingInterval]] as well as the measurement [[timeout]] is configured globally (i.e. as static variables).
*
* ## Power rail activation/deactivation
* The implementation optionally supports a (common) *power rail activation/deactivation*: If a [[sensorPowerPin]] is specified, power to the DHT sensors will be switched on prior to a measurement and switched off after measurements finished (or timed out). The [[sensorWarmUpTime]] determines, how long (in seconds) the software will wait until starting a measurement after power has been activated.
*/
class Thermostat {
/** Constructs a new thermostat based on the supplied configuration. */
constructor(config) {
/** The desired temperature (in °C) to be maintained by the thermostat. */
this._setpoint = 18;
/** UNIX timestamp (in milliseconds) of the latest measurement. */
this._timestamp = undefined;
/** Raw temperature (in °C) of the latest measurement. */
this._rawTemperature = undefined;
/** Temperature (in °C) of the latest measurement; possibly with correction summand applied. */
this._temperature = undefined;
/** Relative humidity (in %) of the latest measurement. */
this._humidity = undefined;
/** Determines whether the heating is currently on. */
this._heatingIsOn = false;
this.internalId = Thermostat.nextInternalId++;
this.name = config.name;
if (config.sensorPin) {
this.sensor = (0, pigpio_dht_1.default)(config.sensorPin, config.sensorType ? config.sensorType : Thermostat.defaultSensorType);
this.sensorPin = config.sensorPin;
this.sensor.on('activate', () => {
// nothing to be done
});
this.sensor.on('badChecksum', () => {
this.onData(undefined, undefined);
});
this.sensor.on('result', (data) => {
this.onData(data.temperature, data.humidity);
});
this.sensor.on('end', () => {
// nothing to be done
// TODO: maybe reduce measurement counter here?
});
}
this.setpoint = config.setpoint
? config.setpoint
: Thermostat.defaultSetpoint;
this.temperatureSummand = config.temperatureSummand
? config.temperatureSummand
: 0;
if (config.actuatorPin) {
this.actuator = new pigpio_1.Gpio(config.actuatorPin, { mode: pigpio_1.Gpio.OUTPUT });
this.actuatorPin = config.actuatorPin;
// disable actuator if no sensor is defined
if (!config.sensorPin) {
this.actuator.digitalWrite(0);
}
}
}
/** GPIO pin controlling the power supply for all connected DHT sensors. If undefined, power is assumed to be always on. */
static get sensorPowerPin() {
return this._sensorPowerPin;
}
/** GPIO pin controlling the power supply for all connected DHT sensors. If undefined, power is assumed to be always on. */
static set sensorPowerPin(pin) {
this._sensorPowerPin = pin;
this._sensorPowerGpio = new pigpio_1.Gpio(pin, {
mode: pigpio_1.Gpio.OUTPUT,
});
}
/** The desired temperature (in °C) to be maintained by the thermostat. */
get setpoint() {
return this._setpoint;
}
/** The desired temperature (in °C) to be maintained by the thermostat. */
set setpoint(temperature) {
this._setpoint = temperature;
void this.check();
}
/** UNIX timestamp (in milliseconds) of the latest measurement. */
get timestamp() {
return this._timestamp;
}
/** Raw temperature (in °C) measured in the latest measurement. */
get rawTemperature() {
return this._rawTemperature;
}
/** Temperature (in °C) measured in the latest measurement; possibly with correction summand applied. */
get temperature() {
return this._temperature;
}
/** Relative humidity (in %) measured in the latest measurement. */
get humidity() {
return this._humidity;
}
/** Determines whether the heating is currently on. */
get heatingIsOn() {
return this._heatingIsOn;
}
/** Determines whether the thermostat is active. */
get isActive() {
return Thermostat.activeInstances.has(this.internalId);
}
/** The main measurement loop.*/
static async main() {
// nothing to be done if not active
if (!this._isActive) {
return;
}
if (this._sensorPowerGpio) {
// power up sensors ...
this._sensorPowerGpio.digitalWrite(1);
// ... and wait for sensor warmup (at least one second)
await (0, delay_1.default)(Math.max(1000, 1000 * this.sensorWarmUpTime));
}
// start measurements
Thermostat.remainingMeasurements = 0;
this.activeInstances.forEach((thermostat) => {
if (thermostat.sensor) {
Thermostat.remainingMeasurements++;
thermostat.sensor.read();
}
});
// wait for measurements to finish
const started = Date.now();
while (Thermostat.remainingMeasurements > 0) {
await (0, delay_1.default)(1000);
const duration = (Date.now() - started) / 1000;
if (duration > this.timeout) {
break;
}
}
if (this._sensorPowerGpio) {
// power down sensors
this._sensorPowerGpio.digitalWrite(0);
}
// determine next activation of this function (at least 2s required)
const wait = Math.max(2000, this.samplingInterval * 1000);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
setTimeout(this.main.bind(this), wait);
}
/** Activates the main measurement loop for all active thermostats. */
static async activate() {
// nothing to be done if already active
if (this._isActive) {
return;
}
this._isActive = true;
// wait
await (0, delay_1.default)(1000);
// run main loop
void this.main();
}
// Deactivates all active thermostats at once.
static deactivateAll() {
this._isActive = false;
if (this._sensorPowerGpio) {
this._sensorPowerGpio.digitalWrite(0);
}
this.activeInstances.forEach((thermostat) => {
thermostat.deactivate();
});
}
/** Handles new temperature/humidity data. */
onData(temperature, humidity) {
Thermostat.remainingMeasurements--;
this._timestamp = Date.now();
this._rawTemperature = Math.round(temperature * 10) / 10;
this._temperature = this._rawTemperature + this.temperatureSummand;
this._humidity = Math.round(humidity);
void this.check();
}
/** Checks if heating needs to be switched on/off */
async check() {
if (!this.actuator) {
this._heatingIsOn = false;
return;
}
if (this.temperature) {
// heating is on iff measured temperatore does not yet exceed setpoint
this._heatingIsOn = this.temperature >= this.setpoint ? false : true;
// TODO: implement hysteresis to prevent too frequent switching
}
else {
// if measurement failed, switch off heating
this._heatingIsOn = false;
}
// to prevent simultaneous relay switching, add random delay of up to 500ms
const ms = Math.random() * 500;
await (0, delay_1.default)(ms);
this.actuator.digitalWrite(this.heatingIsOn === true ? 1 : 0);
}
/** Activates the thermostat. */
activate() {
// cannot activate an already active thermostat
if (Thermostat.activeInstances.has(this.internalId)) {
return;
}
// add to list of active instances
Thermostat.activeInstances.set(this.internalId, this);
//activate main loop
void Thermostat.activate();
}
/** Deactivates the thermostat.
* @param heatingOn Determines whether after deactivation of the thermostat the heating is off (default) or on (if set to true).
*/
deactivate(heatingOn = false) {
// remove from list of active instances
Thermostat.activeInstances.delete(this.internalId);
if (Thermostat.activeInstances.size < 1) {
Thermostat._isActive = false;
}
// switch heating on/off
if (this.actuator) {
this.actuator.digitalWrite(heatingOn ? 1 : 0);
}
}
configurationToJSON() {
return {
name: this.name,
sensorPin: this.sensorPin,
sensorType: this.sensorType,
actuatorPin: this.actuatorPin,
setpoint: this.setpoint,
temperatureSummand: this.temperatureSummand,
};
}
toJSON() {
return {
name: this.name,
setpoint: this.setpoint,
timestamp: this.timestamp,
rawTemperature: this.rawTemperature,
temperature: this.temperature,
humidity: this.humidity,
heatingIsOn: this.heatingIsOn,
};
}
toString() {
return JSON.stringify(this.toJSON());
}
}
exports.Thermostat = Thermostat;
/** Sampling interval (in seconds) for all thermostats.
*
* This is the interval ranging from
* - the finalization (or timeout) of a measurement run to
* - the next start of the measurement run. */
Thermostat.samplingInterval = 120;
/** Delay (in seconds) between sensor power on and start of measurements. */
Thermostat.sensorWarmUpTime = 4;
/** Timeout after which all measurements will be stopped. */
Thermostat.timeout = 5;
/** The default temperature (in °C) to be maintained by a newly created thermostat, if no such value is configured on creation. */
Thermostat.defaultSetpoint = 18;
/** The default sensor type for a new thermostat. */
Thermostat.defaultSensorType = 22;
Thermostat.remainingMeasurements = 0;
Thermostat.activeInstances = new Map();
Thermostat.nextInternalId = 0;
// Private members
/** GPIO pin controlling the power supply for all connected DHT sensors. If undefined, power is assumed to be always on. */
Thermostat._sensorPowerPin = undefined;
/** GPIO controlling the power supply for all connected DHT sensors. If undefined, power is assumed to be always on. */
Thermostat._sensorPowerGpio = undefined;
/** Determines whether the thermostat is active. */
Thermostat._isActive = false;
//# sourceMappingURL=Thermostat.js.map