vastra-radiator-valve
Version:
Node.js library to query and configure Vastra's smart radiator valves.
153 lines (131 loc) • 4.24 kB
text/typescript
import { EventEmitter } from "events";
import { IGattCentral } from "./bluetooth";
import Logger from "./logger";
import RadiatorValve from "./valve";
export type RadiatorValvesOptions = {
wakeUpInterval: number;
maxConnectionAttempts: number;
connectionTimeout: number;
maxReadAttempts: number;
readTimeout: number;
maxWriteAttempts: number;
logger?: Logger;
verbose?: boolean;
/**
* Whether to automatically connect to the peripheral once it is
* discovered. A `connected` event will be emitted once the connection
* is established. If this is disabled, then only the `discovered` event
* will be emitted.
*/
autoConnect?: boolean;
/**
* Whether to enable notification flag in client configuration descriptor
* of the RX characteristic. Some drivers do it automatically, so it's not
* always necessary.
*/
raspberryFix: boolean;
};
export declare interface RadiatorValveScanner {
emit(event: "connected", valve: RadiatorValve): boolean;
emit(event: "discovered", valve: RadiatorValve): boolean;
on(event: "connected", listener: (valve: RadiatorValve) => void): this;
on(event: "discovered", listener: (valve: RadiatorValve) => void): this;
}
export class RadiatorValveScanner extends EventEmitter {
private connectedValves: Array<RadiatorValve> = [];
private connectQueue: Array<RadiatorValve> = [];
private connecting = false;
private scanning = false;
constructor(
private readonly bluetooth: IGattCentral,
private readonly options: Partial<RadiatorValvesOptions> = {}
) {
super();
this.options = {
wakeUpInterval: 10 * 1000,
maxConnectionAttempts: 5,
connectionTimeout: 7000,
maxReadAttempts: 5,
readTimeout: 5000,
maxWriteAttempts: 5,
logger: new Logger(options.verbose ?? true),
raspberryFix: false,
autoConnect: true,
...options,
};
}
public async disconnectAll() {
await this.stop();
for (const valve of this.connectedValves) {
await valve.disconnect();
}
this.connectedValves = [];
}
public findOne(): Promise<RadiatorValve> {
return new Promise((resolve) => {
this.bluetooth.startScanning(async (peripheral) => {
await this.bluetooth.stopScanning();
const valve = new RadiatorValve(peripheral, this.options as RadiatorValvesOptions);
if (this.options.autoConnect) {
await valve.connect();
}
resolve(valve);
});
});
}
public start() {
this.scanning = true;
return this.bluetooth.startScanning((peripheral) => {
const valve = new RadiatorValve(peripheral, this.options as RadiatorValvesOptions);
this.emit("discovered", valve);
if (this.options.autoConnect) {
this.queueConnect(valve);
}
});
}
public stop() {
if (!this.scanning) {
return Promise.resolve();
}
this.scanning = false;
return this.bluetooth.stopScanning();
}
private queueConnect(valve: RadiatorValve) {
this.connectQueue.push(valve);
this.processConnectQueue();
}
private async processConnectQueue() {
if (this.connecting) {
return;
}
const valve = this.connectQueue.shift();
if (!valve) {
return;
}
this.connecting = true;
await this.stop();
try {
await valve.connect();
this.connectedValves.push(valve);
if (!this.emit("connected", valve)) {
this.options.logger?.debug(
`Disconnecting ${valve.peripheral.address} because there are no listeners`
);
await valve.disconnect();
}
} catch (error) {
// Disconnect in case the error was thrown after the connection was established,
// but before it was confirmed (e.g. we failed to discover services or characteristics)
await valve.disconnect();
this.options.logger?.error(String(error));
}
this.connecting = false;
if (this.connectQueue.length > 0) {
this.processConnectQueue();
} else {
if (this.scanning) {
this.start();
}
}
}
}