node-mi-flora-classic-updated
Version:
Node package for xiaomi mi flora plant sensor. Version 0.1.1 of original with update to support abandonware version of noble.
123 lines (109 loc) • 4.15 kB
JavaScript
const EventEmitter = require('events');
const noble = require('@abandonware/noble');
const debug = require('debug')('miflora');
const DeviceData = require('./lib/device-data');
const DEFAULT_DEVICE_NAME = 'Flower care';
const DATA_SERVICE_UUID = '0000120400001000800000805f9b34fb';
const DATA_CHARACTERISTIC_UUID = '00001a0100001000800000805f9b34fb';
const FIRMWARE_CHARACTERISTIC_UUID = '00001a0200001000800000805f9b34fb';
const REALTIME_CHARACTERISTIC_UUID = '00001a0000001000800000805f9b34fb';
const REALTIME_META_VALUE = Buffer.from([0xA0, 0x1F]);
const SERVICE_UUIDS = [DATA_SERVICE_UUID];
const CHARACTERISTIC_UUIDS = [DATA_CHARACTERISTIC_UUID, FIRMWARE_CHARACTERISTIC_UUID, REALTIME_CHARACTERISTIC_UUID];
class MiFlora extends EventEmitter {
constructor(macAddress) {
super();
this.noble = noble;
this._macAddress = macAddress;
noble.on('discover', this.discover.bind(this));
}
discover(peripheral) {
debug('device discovered: ', peripheral.advertisement.localName);
if (this._macAddress !== undefined) {
if (this._macAddress.toLowerCase() === peripheral.address.toLowerCase()) {
debug('trying to connect mi flora, living at %s', this._macAddress);
// start listening the specific device
this.connectDevice(peripheral);
}
} else if (peripheral.advertisement.localName === DEFAULT_DEVICE_NAME) {
debug('no mac address specified, trying to connect available mi flora...');
// start listening found device
this.connectDevice(peripheral);
}
}
connectDevice(peripheral) {
// prevent simultanious connection to the same device
if (peripheral.state === 'disconnected') {
peripheral.connect();
peripheral.once('connect', function () {
this.listenDevice(peripheral, this);
}.bind(this));
}
}
listenDevice(peripheral, context) {
peripheral.discoverSomeServicesAndCharacteristics(SERVICE_UUIDS, CHARACTERISTIC_UUIDS, function (error, services, characteristics) {
characteristics.forEach(function (characteristic) {
switch (characteristic.uuid) {
case DATA_CHARACTERISTIC_UUID:
characteristic.read(function (error, data) {
context.parseData(peripheral, data);
});
break;
case FIRMWARE_CHARACTERISTIC_UUID:
characteristic.read(function (error, data) {
context.parseFirmwareData(peripheral, data);
});
break;
case REALTIME_CHARACTERISTIC_UUID:
debug('enabling realtime');
characteristic.write(REALTIME_META_VALUE, false);
break;
default:
debug('found characteristic uuid %s but not matched the criteria', characteristic.uuid);
}
});
});
}
parseData(peripheral, data) {
debug('data:', data);
let temperature = data.readUInt16LE(0) / 10;
let lux = data.readUInt32LE(3);
let moisture = data.readUInt16BE(6);
let fertility = data.readUInt16LE(8);
let deviceData = new DeviceData(peripheral.id,
temperature,
lux,
moisture,
fertility);
debug('temperature: %s °C', temperature);
debug('Light: %s lux', lux);
debug('moisture: %s %', moisture);
debug('fertility: %s µS/cm', fertility);
this.emit('data', deviceData);
}
parseFirmwareData(peripheral, data) {
debug('firmware data:', data);
let firmware = {
deviceId: peripheral.id,
batteryLevel: parseInt(data.toString('hex', 0, 1), 16),
firmwareVersion: data.toString('ascii', 2, data.length)
};
this.emit('firmware', firmware);
}
startScanning() {
if (noble.state === 'poweredOn') {
noble.startScanning([], true);
} else {
// bind event to start scanning
noble.on('stateChange', function (state) {
if (state === 'poweredOn') {
noble.startScanning([], true);
}
});
}
}
stopScanning() {
noble.stopScanning();
}
}
module.exports = MiFlora;