UNPKG

bt-sensors-plugin-sk

Version:

Bluetooth Sensors for Signalk -- support for Victron devices, RuuviTag, Xiaomi, ATC and Inkbird, Ultrasonic wind meters, Mopeka tank readers, Renogy Battery and Solar Controllers, Aranet4 environment sensors, SwitchBot temp and humidity sensors, Kilovault

245 lines (222 loc) 11 kB
const BTSensor = require("../BTSensor"); const crypto = require('crypto'); const util = require('util'); const DEVICE_TYPES = new Map([ [0x0C3C, { name: "Alarm Clock", model: "CGC1" }], [0x0576, { name: "3-in-1 Alarm Clock", model: "CGD1" }], [0x066F, { name: "Temperature/Humidity Sensor", model: "CGDK2" }], [0x0347, { name: "Temperature/Humidity Sensor", model: "CGG1" }], [0x0B48, { name: "Temperature/Humidity Sensor", model: "CGG1-ENCRYPTED" }], [0x03D6, { name: "Door/Window Sensor", model: "CGH1" }], [0x0A83, { name: "Motion/Light Sensor", model: "CGPR1" }], [0x03BC, { name: "Grow Care Garden", model: "GCLS002" }], [0x0098, { name: "Plant Sensor", model: "HHCCJCY01" }], [0x015D, { name: "Smart Flower Pot", model: "HHCCPOT002" }], [0x02DF, { name: "Formaldehyde Sensor", model: "JQJCY01YM" }], [0x0997, { name: "Smoke Detector", model: "JTYJGD03MI" }], [0x1568, { name: "Switch (single button)", model: "K9B-1BTN" }], [0x1569, { name: "Switch (double button)", model: "K9B-2BTN" }], [0x0DFD, { name: "Switch (triple button)", model: "K9B-3BTN" }], [0x1C10, { name: "Switch (single button)", model: "K9BB-1BTN" }], [0x1889, { name: "Door/Window Sensor", model: "MS1BB(MI)" }], [0x2AEB, { name: "Motion Sensor", model: "HS1BB(MI)" }], [0x3F0F, { name: "Flood and Rain Sensor", model: "RS1BB(MI)" }], [0x01AA, { name: "Temperature/Humidity Sensor", model: "LYWSDCGQ" }], [0x045B, { name: "Temperature/Humidity Sensor", model: "LYWSD02" }], [0x16E4, { name: "Temperature/Humidity Sensor", model: "LYWSD02MMC" }], [0x2542, { name: "Temperature/Humidity Sensor", model: "LYWSD02MMC" }], [0x055B, { name: "Temperature/Humidity Sensor", model: "LYWSD03MMC" }], [0x2832, { name: "Temperature/Humidity Sensor", model: "MJWSD05MMC" }], [0x098B, { name: "Door/Window Sensor", model: "MCCGQ02HL" }], [0x06D3, { name: "Alarm Clock", model: "MHO-C303" }], [0x0387, { name: "Temperature/Humidity Sensor", model: "MHO-C401" }], [0x07F6, { name: "Nightlight", model: "MJYD02YL" }], [0x04E9, { name: "Door Lock", model: "MJZNMSQ01YD" }], [0x00DB, { name: "Baby Thermometer", model: "MMC-T201-1" }], [0x0391, { name: "Body Thermometer", model: "MMC-W505" }], [0x03DD, { name: "Nightlight", model: "MUE4094RT" }], [0x0489, { name: "Smart Toothbrush", model: "M1S-T500" }], [0x0806, { name: "Smart Toothbrush", model: "T700" }], [0x1790, { name: "Smart Toothbrush", model: "T700" }], [0x0A8D, { name: "Motion Sensor", model: "RTCGQ02LM" }], [0x3531, { name: "Motion Sensor", model: "XMPIRO2SXS" }], [0x4C60, { name: "Motion Sensor", model: "XMPIRO2GSXS" }], [0x4683, { name: "Occupancy Sensor", model: "XMOSB01XS" }], [0x0863, { name: "Flood Detector", model: "SJWS01LM" }], [0x045C, { name: "Smart Kettle", model: "V-SK152" }], [0x040A, { name: "Mosquito Repellent", model: "WX08ZM" }], [0x04E1, { name: "Magic Cube", model: "XMMF01JQD" }], [0x1203, { name: "Thermometer", model: "XMWSDJ04MMC" }], [0x1949, { name: "Switch (double button)", model: "XMWXKG01YL" }], [0x2387, { name: "Button", model: "XMWXKG01LM" }], [0x098C, { name: "Door Lock", model: "XMZNMST02YD" }], [0x0784, { name: "Door Lock", model: "XMZNMS04LM" }], [0x0E39, { name: "Door Lock", model: "XMZNMS08LM" }], [0x07BF, { name: "Wireless Switch", model: "YLAI003" }], [0x38BB, { name: "Wireless Switch", model: "PTX_YK1_QMIMB" }], [0x0153, { name: "Remote Control", model: "YLYK01YL" }], [0x068E, { name: "Fan Remote Control", model: "YLYK01YL-FANCL" }], [0x04E6, { name: "Ventilator Fan Remote Control", model: "YLYK01YL-VENFAN" }], [0x03BF, { name: "Bathroom Heater Remote", model: "YLYB01YL-BHFRC" }], [0x03B6, { name: "Dimmer Switch", model: "YLKG07YL/YLKG08YL" }], [0x0083, { name: "Smart Kettle", model: "YM-K1501" }], [0x0113, { name: "Smart Kettle", model: "YM-K1501EU" }], [0x069E, { name: "Door Lock", model: "ZNMS16LM" }], [0x069F, { name: "Door Lock", model: "ZNMS17LM" }], [0x0380, { name: "Door Lock", model: "DSL-C08" }], [0x11C2, { name: "Door Lock", model: "Lockin-SV40" }], [0x0DE7, { name: "Odor Eliminator", model: "SU001-T" }] ]); class XiaomiMiBeacon extends BTSensor{ constructor(device, config, gattConfig){ super(device, config, gattConfig) this.encryptionKey = config?.encryptionKey } static SERVICE_MIBEACON = "0000fe95-0000-1000-8000-00805f9b34fb" static async identify(device){ var sd = await this.getDeviceProp(device, 'ServiceData') if (sd == null || sd.length==0) return null else{ const keys = Object.keys(sd) if (keys[0]==this.SERVICE_MIBEACON) return this else return null } } GATTwarning = "WARNING: Xiaomi GATT connections are known to be unreliable on Debian distributions with Bluez 5.55 and up (earlier BlueZ versions are untested). Using GATT on the Xiaomi may put the system Bluetooth stack into an inconsistent state disrupting and disabling other plugin Bluetooth connections. If by some chance you're successful using GATT with the Xiaomi, let the plugin developer know your configuration. Refer to the plugin documentation for getting the Xiamoi bindKey for non-GATT connections and more information on Xiaomi GATT issues." emitValues(buffer){ this.emitData("temp", buffer, 0) this.emit("humidity", buffer.readUInt8(2)/100) this.emitData("voltage",buffer,3); } getManufacturer(){ return "Xiaomi Inc." } getGATTDescription() { return this.GATTwarning } initGATTConnection(){ if (!this?.gattWarningDelivered) { this.debug(this.GATTwarning.toUpperCase()) this.gattWarningDelivered=true } return new Promise((resolve,reject )=>{ this.device.connect().then(async ()=>{ if (!this.gattServer) { this.gattServer = await this.device.gatt() this.gattService = await this.gattServer.getPrimaryService("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6") this.gattCharacteristic = await this.gattService.getCharacteristic("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6") } resolve(this) }) .catch((e)=>{ reject(e.message) }) }) } emitGATT(){ this.gattCharacteristic.readValue() .then((buffer)=>this.emitValues(buffer)) } async initGATTNotifications() { Promise.resolve(this.gattCharacteristic.startNotifications().then(()=>{ this.gattCharacteristic.on('valuechanged', buffer => { this.emitValues(buffer) }) })) } decryptV2and3(data){ const encryptedPayload = data.subarray(-4); const xiaomi_mac = data.subarray(5,11) const nonce = Buffer.concat([data.subarray(0, 5), data.subarray(-4,-1), xiaomi_mac.subarray(-1)]); const cipher = crypto.createDecipheriv('aes-128-ccm', Buffer.from(this.encryptionKey,"hex"), nonce, { authTagLength: 4}); cipher.setAAD(Buffer.from('11', 'hex'), { plaintextLength: encryptedPayload.length }); return cipher.update(encryptedPayload) } decryptV4and5(data){ const encryptedPayload = data.subarray(11,-7); const xiaomi_mac = data.subarray(5,11) const nonce = Buffer.concat([xiaomi_mac, data.subarray(2, 5), data.subarray(-7,-4)]); const cipher = crypto.createDecipheriv('aes-128-ccm', Buffer.from(this.encryptionKey,"hex"), nonce, { authTagLength: 4}); cipher.setAAD(Buffer.from('11', 'hex'), { plaintextLength: encryptedPayload.length }); cipher.setAuthTag(data.subarray(-4)) return cipher.update(encryptedPayload) } hasGATT(){ return true } propertiesChanged(props){ super.propertiesChanged(props) if (this.usingGATT()) return if (!props.hasOwnProperty("ServiceData")) return const data = this.getServiceData(this.constructor.SERVICE_MIBEACON) var dec if (!this.encryptionKey){ throw new Error(`${this.getNameAndAddress()} requires an encryption key.`) } if (this.encryptionVersion >= 4) { dec = this.decryptV4and5(data) } else { if(this.encryptionVersion>=2){ dec=this.decryptV2and3(data) } } if (dec.length==0) throw new Error(`${this.getNameAndAddress()} received empty decrypted packet. Check that the bind/encryption key in config is correct.`) switch(dec[0]){ case 0x0D: this.emitData("temp",dec,3) this.emitData("humidity",dec,5) break case 0x0A: this.emitData("batteryStrength",dec,3) break case 0x04: this.emitData("temp",dec,3) break case 0x06: this.emitData("humidity",dec,3) break default: throw new Error(`${this.getNameAndAddress()} unable to parse decrypted service data (${util.inspect(dec)})`) } } async init(){ await super.init() const md = this.addMetadatum("encryptionKey", "", "encryptionKey (AKA bindKey) for decryption") md.isParam=true this.addMetadatum('temp','K', 'temperature', (buff,offset)=>{return ((buff.readInt16LE(offset))/10) + 273.15}) this.addMetadatum('humidity','ratio', 'humidity', (buff,offset)=>{return buff.readInt16LE(offset)/1000}) this.addMetadatum('batteryStrength', 'ratio', 'sensor battery strength', (buff,offset)=>{return ((buff.readUInt8(offset))/100)}) this.addMetadatum('voltage', 'V', 'sensor battery voltage', (buff,offset)=>{return ((buff.readUInt16LE(offset))/1000)}) const data = this.getServiceData(this.constructor.SERVICE_MIBEACON) const frameControl = data[0] + (data[1] << 8) this.deviceID = data[2] + (data[3] << 8) this.isEncrypted = (frameControl >> 3) & 1 this.encryptionVersion = frameControl >> 12 } getName(){ const dt = DEVICE_TYPES.get(this.deviceID) return this?.name??`${dt.name} ${dt.model}` } async disconnectGATTCharacteristic(){ if (this.gattCharacteristic && await this.gattCharacteristic.isNotifying()) { await this.gattCharacteristic.stopNotifications() this.gattCharacteristic=null } } async stopListening(){ super.stopListening() await this.disconnectGATTCharacteristic() if (await this.device.isConnected()){ await this.device.disconnect() } } } module.exports=XiaomiMiBeacon