bt-sensors-plugin-sk
Version:
Bluetooth Sensors for Signalk - see https://www.npmjs.com/package/bt-sensors-plugin-sk for a list of supported classes
261 lines (232 loc) • 11.4 kB
JavaScript
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
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
}
}
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> WARNING: If you use the GATT connection, you won't need an encrypytion//bind key to get your data but the energy cost of maintaining a GATT connection is high and will drain your battery in about 2 weeks of persistent use. Instead 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 ""
}
initGATTConnection(){
return new Promise((resolve,reject )=>{
this.deviceConnect().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 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
}
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 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