@dr_chai/homebridge-airthings
Version:
Connecting all your Airthings devices over bluetooth with zero configuration.
182 lines • 7.42 kB
JavaScript
import { parseSerial, parseWave2Rawdata, WAVE2_CURR_VAL_UUID, } from './parser.js';
// Utility function to simulate sleep
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
export default class {
log;
curState = 'unknown';
isScanning = false;
discoveredDeivces = new Map();
discoveredPeripherals = new Map();
lastData = new Map();
stopRunner = false;
config;
startScanning;
stopScanning;
constructor(bleConfig, log) {
this.log = log;
// Configs
this.config = {
...bleConfig,
scanTime: bleConfig.scanTime < 10 * 1000 ? 10 * 1000 : bleConfig.scanTime,
refreshTime: bleConfig.refreshTime < 5 * 60 * 1000
? 5 * 60 * 1000
: bleConfig.refreshTime,
};
this.startScanning = () => {
throw new Error('[BLE] Bluetooth(noble) is not available');
};
this.stopScanning = () => {
throw new Error('[BLE] Bluetooth(noble) is not available');
};
this.initNoble()
.then(() => this.log.info('[BLE] Bluetooth(noble) is ready!'))
.catch((err) => {
this.log.error('[BLE] Bluetooth(noble) is not available');
this.log.error(err);
});
}
async initNoble() {
const noble = (await import('@abandonware/noble')).default;
noble.on('scanStart', () => {
this.log.debug('[BLE] starting the discover.');
});
noble.on('scanStop', () => {
this.log.debug('[BLE] stopped the discover.');
});
noble.on('stateChange', (state) => {
this.curState = state;
if (state === 'poweredOn') {
this.log.debug('[BLE] Adapter is powered on.');
}
else {
this.log.error('[BLE] %s.', state);
this.stopScanning();
}
});
noble.on('discover', this.sensorStartDiscovery);
this.startScanning = () => {
this.isScanning = true;
noble.startScanning([], true);
};
this.stopScanning = () => {
this.isScanning = false;
noble.stopScanning();
};
}
getValidatedDevices = async () => {
if (this.curState !== 'poweredOn') {
this.log.debug('[BLE] Adapter is not powered on. Waiting for state to change...');
await new Promise((resolve) => {
const checkState = () => {
if (this.curState === 'poweredOn') {
resolve(null);
}
else {
this.log.info(`[BLE] bluetooth state: ${this.curState}. rechecking after ${this.config.retryAfter / 1000}s.`);
setTimeout(checkState, this.config.retryAfter);
}
};
checkState();
});
}
this.startScanning();
await new Promise((resolve) => {
setTimeout(() => {
this.stopScanning();
this.log.debug(`[BLE] Scan complete. after scanTime: ${this.config.scanTime / 1000}s`);
resolve(null);
}, this.config.scanTime);
});
if (this.discoveredDeivces.size === 0) {
this.log.error(`[BLE] No devices found. Retrying after${this.config.retryAfter / 1000}`);
await sleep(this.config.retryAfter);
return this.getValidatedDevices();
}
return this.discoveredDeivces;
};
sensorStartDiscovery = (peripheral) => {
const { advertisement: { manufacturerData, localName } = {}, id, address, } = peripheral;
const sn = manufacturerData && parseSerial(manufacturerData);
if (sn && !this.discoveredDeivces.has(sn.toString())) {
this.discoveredPeripherals.set(sn.toString(), peripheral);
peripheral.on('connect', (error) => this.log.debug(`[BLE] [${sn.toString()}] connected with: ${error}`));
peripheral.on('disconnect', (error) => this.log.debug(`[BLE] [${sn.toString()}] disconnected with: ${error}`));
this.discoveredDeivces.set(sn.toString(), {
sn: sn.toString(),
id: address || id,
displayName: localName || 'Default',
});
}
};
startRunner = async () => {
// Check if the stop flag is set
if (this.stopRunner) {
this.log.debug('[BLE] Runner stopped.');
return;
}
for (const [sn, device] of this.discoveredDeivces) {
try {
// Wait for either getData to complete or timeout after 5 seconds
await Promise.race([
sleep(this.config.scanTime),
this.getData(device), // Fetch data
]);
}
catch (error) {
this.log.error(`[BLE] Error getting data for ${sn}. current timeout in ${this.config.scanTime}:`, error);
}
}
this.log.debug(`[BLE] Next sync will be scheduled in ${this.config.refreshTime / 1000 / 60}mins. `);
await sleep(this.config.refreshTime);
this.startRunner();
};
getData = async (device) => {
const peripheral = this.discoveredPeripherals.get(device.sn);
if (!peripheral) {
this.log.error(`[BLE] Peripheral not found for device ${device.sn}`);
throw new Error(`Peripheral not found for device ${device.sn}`);
}
if (peripheral.state !== 'disconnected') {
this.log.warn(`Peripheral state is "${peripheral.state}".`);
switch (peripheral.state) {
case 'connecting':
// consider awaitable lock here
//peripheral.cancelConnect();
throw new Error('other accessories is requsted to connect already');
case 'connected':
return this.lastData.get(device.sn);
case 'error':
this.log.error(`[BLE] Peripheral ${device.sn} has an error state. Refreshing devices.`);
this.clear();
await this.getValidatedDevices();
break;
}
}
// consider add awaitable lock here
await peripheral.connectAsync();
const char = await peripheral.discoverSomeServicesAndCharacteristicsAsync([], [WAVE2_CURR_VAL_UUID]);
const buf = await char.characteristics[0].readAsync();
await this.disconnect(peripheral);
const data = parseWave2Rawdata(buf);
this.lastData.set(device.sn, data);
this.log.debug(`[BLE] received ${device.sn} Data:`, data);
return this.lastData.get(device.sn);
};
disconnect = async (peripheral) => {
this.stopScanning();
await Promise.race([sleep(1000 * 5), peripheral.disconnectAsync()]);
};
// Method to stop the runner
stop = () => {
this.stopRunner = true;
this.stopScanning();
this.log.debug('[BLE] Stop flag set. Runner will stop after current iteration.');
};
clear = () => {
this.stop();
this.discoveredDeivces.clear();
this.discoveredPeripherals.clear();
this.lastData.clear();
};
}
//# sourceMappingURL=ble.js.map