UNPKG

bt-sensors-plugin-sk

Version:

Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk#supported-sensors for a list of supported sensors

275 lines (239 loc) 11.9 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{ static Domain = BTSensor.SensorDomains.environmental static ImageFile = "LYWSD03MMC-Device.jpg" 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 } } static ImageFile="LYWSD03MMC-Device.jpg" emitValues(buffer){ this.emitData("temp", buffer, 0) this.emit("humidity", buffer.readUInt8(2)/100) this.emitData("voltage",buffer,3); } getDescription(){ return `<div><p><img src="../bt-sensors-plugin-sk/images/LYWSD03MMC-Device.jpg" alt=LYWSD03MMC image" style="float: left; margin-right: 10px;" /> The LYWSD03MMC temperature and humidity sensor is an inexpensive environmental sensor from Xiaomi Inc. <p> Follow the instructions <a href=https://github.com/PiotrMachowski/Xiaomi-cloud-tokens-extractor/?tab=readme-ov-file#linux--home-assistant-in-ssh--web-terminal target="_blank">here</a> to get your device's encryption key.<div>` } getManufacturer(){ return "Xiaomi Inc." } getGATTDescription() { return "" } async initGATTConnection(isReconnecting){ await super.initGATTConnection(isReconnecting) const gatt = await this.getGATTServer() const gattService = await gatt.getPrimaryService("ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6") this.gattCharacteristic = await gattService.getCharacteristic("ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6") } emitGATT(){ this.gattCharacteristic.readValue() .then( (buffer)=>this.emitValues(buffer) ) } async initGATTNotifications() { await this.gattCharacteristic.startNotifications() this.gattCharacteristic.on('valuechanged', buffer => { this.emitValues(buffer) }) } decryptV2and3(data, index=11){ const encryptedPayload = data.subarray(-4); const nonce = Buffer.concat([data.subarray(0, 5), data.subarray(-4,-1), (this._mac_reversed)]); 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, index=11){ const encryptedPayload = data.subarray(index,-7); const nonce = Buffer.concat([(this._mac_reversed), 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 false } propertiesChanged(props){ super.propertiesChanged(props) if (this.usingGATT()) return if (!props.hasOwnProperty("ServiceData")) return const data = this.getServiceData(this.constructor.SERVICE_MIBEACON) if (data.length<12) return let dec=[] let dataIndex=5 const frameControl = data.readUInt16LE(0) const isEncrypted = (frameControl >> 3) & 1 const encryptionVersion = frameControl >> 12 const mesh = (frameControl >> 7) & 1; // mesh device const authMode = (frameControl >> 10) & 3; const solicited = (frameControl >> 9) & 1; const registered = (frameControl >> 8) & 1; const objectInclude = (frameControl >> 6) & 1; // object/payload data present const capabilityInclude = (frameControl >> 5) & 1; // capability byte present const macInclude = (frameControl >> 4) & 1; // MAC address included in payload const requestTiming = frameControl & 1; if (!this.encryptionKey){ throw new Error(`${this.getNameAndAddress()} requires an encryption key.`) } if (encryptionVersion >= 4) { dec = this.decryptV4and5(data, macInclude?11:5) } else { if(encryptionVersion>=2){ dec=this.decryptV2and3(data, macInclude?11:5) } } if (dec.length==0) throw new Error(`${this.getNameAndAddress()} received empty decrypted packet. Check that the bind/encryption key in config is correct.`) const objCode = dec.readUInt16LE(0) switch(objCode){ case 0x100D: this.emitData("temp",dec,3) this.emitData("humidity",dec,5) break case 0x100A: this.emitData("batteryStrength",dec,3) break case 0x1004: this.emitData("temp",dec,3) break case 0x1006: this.emitData("humidity",dec,3) break case 0x4C01: this.emit("temp",dec.readFloatLE(3)+273.15) break case 0x4C02: this.emit("humidity",dec[3]/100) break case 0x4C03: this.emit("batteryStrength",dec[3]/100) break default: throw new Error(`${this.getNameAndAddress()} unable to parse decrypted service data (${util.inspect(dec)})`) } } async init(){ await super.init() this._mac_reversed = (Buffer.from(this.getMacAddress().replaceAll(":",""), "hex")).reverse() const data = this.getServiceData(this.constructor.SERVICE_MIBEACON) if (!data || data.length<4) this.setError(`Service Data ${this.constructor.SERVICE_MIBEACON} not available for ${this.getName()}`) else this.deviceID = data[2] + (data[3] << 8) } initSchema(){ super.initSchema() this.addDefaultParam("zone") this.addParameter( "encryptionKey", { title: "encryptionKey (AKA bindKey) for decryption", } ) this.addDefaultPath('temp','environment.temperature') .read=(buff,offset)=>{return ((buff.readInt16LE(offset))/10) + 273.15} this.addDefaultPath('humidity','environment.humidity') .read=(buff,offset)=>{return buff.readInt16LE(offset)/1000} this.addDefaultPath("batteryStrength", "sensors.batteryStrength") .read= (buff,offset)=>{return ((buff.readUInt8(offset))/100)} this.addDefaultPath("voltage", "sensors.batteryVoltage") .read=(buff,offset)=>{return ((buff.readUInt16LE(offset))/1000)} } getName(){ const dt = DEVICE_TYPES.get(this.deviceID) if (!dt) return super.getName() else return this?.name??`${dt.name} ${dt.model}` } async deactivateGATT(){ await this.stopGATTNotifications(this.gattCharacteristic) await super.deactivateGATT() } } module.exports=XiaomiMiBeacon