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

183 lines (153 loc) 5.71 kB
const VC = require( './VictronConstants.js'); const Images = require('./VictronImages.js') const BTSensor = require("../../BTSensor.js"); const crypto = require('node:crypto'); const VictronIdentifier = require('./VictronIdentifier.js'); class VictronSensor extends BTSensor{ static Domain = BTSensor.SensorDomains.electrical static ManufacturerID = 0x2e1 constructor(device,config,gattConfig){ super(device,config,gattConfig) if (device&&device.modelID) this.modelID=device.modelID } static async getDataPacket(device, md, timeout=60000) { if (md && md[this.ManufacturerID]?.value[0]==0x10) return md[this.ManufacturerID].value device.helper._prepare() return new Promise((resolve, reject) => { const timeoutID = setTimeout(() => { device.helper.removeListeners() reject(new Error("Timeout waiting for Victron Manufacturer Data")) }, timeout); device.helper.on("PropertiesChanged", (props)=> { if (Object.hasOwn(props,'ManufacturerData')){ const md = props['ManufacturerData'].value if(md[this.ManufacturerID].value[0]==0x10) { device.helper.removeListeners() clearTimeout(timeoutID); resolve(md[this.ManufacturerID].value) } } }) }) } static getModelID(md) { return md[this.ManufacturerID]?.value.readUInt16LE(2)??-1 } static async identify(device){ var md = await this.getDeviceProp(device,'ManufacturerData') if (md==undefined || !Object.hasOwn(md,this.ManufacturerID)) return null const data=await this.getDataPacket(device,md) if (data) { device.modelID=this.getModelID(md) return VictronIdentifier.identify(data) } return null } offReasonText(value){ let reasons=[]; VC.OffReasons.forEach((v,k)=>{ if (k&value) reasons.push(v); }) return reasons.join("|"); } alarmReasonText(value){ let reasons=[]; VC.AlarmReason.forEach((v,k)=>{ if (k&value) reasons.push(v); }) return reasons.join("|"); } emitAlarm(tag="alarm", alarm){ this.emit( tag, { message:this.alarmReasonText(alarm), alarm: `0x${alarm.toString(16).padStart(8,"0")}`, alarmstate: 'alert'}) } async init(){ await super.init() this.addParameter( "encryptionKey", { title:"Encryption Key", isRequired: true } ) } alarmReason(alarmValue){ return VC.AlarmReason.get(alarmValue) } getModelName(){ const mID = this.getModelID() const m = VC.MODEL_ID_MAP[mID] if(m) { if(typeof m == 'string' || m instanceof String ) { return m } else { return m.name } } return this.constructor.name+` (Model ID: ${mID==-1?"Unknown":mID})` } decrypt(data){ if (!this.encryptionKey) throw new Error("Unable to decrypt: no encryption key set") const encMethod = 'aes-128-ctr'; const iv = data.readUInt16LE(5); const key = Buffer.from(this.encryptionKey,'hex') const ivBuffer = Buffer.alloc(16); // 128 bits = 16 bytes ivBuffer.writeUInt16LE(iv) var encData = Buffer.from([...data.slice(8)]) const decipher = crypto.createDecipheriv(encMethod, key, ivBuffer) const decData=decipher.update(encData) return Buffer.from(decData) } getModelID(){ if (!this.modelID ||this.modelID==-1) this.modelID=this.getManufacturerData(this.constructor.ManufacturerID)?.readUInt16LE(2)??-1 return this.modelID } getName(){ return `Victron ${this.getModelName()}` } propertiesChanged(props){ super.propertiesChanged(props) if (this.usingGATT()) return if (!props.hasOwnProperty("ManufacturerData")) return try{ const md = this.getManufacturerData(this.constructor.ManufacturerID) if (md && md.length && md[0]==0x10){ const decData=this.decrypt(md) this.emitValuesFrom(decData) } } catch (error) { throw new Error(`Unable to read data from ${ this.getDisplayName()}: ${error}` ) } } initGATTConnection(){ throw new Error( "GATT Connection unimplemented for "+this.getDisplayName()) } getImage(){ const m = VC.MODEL_ID_MAP[this.getModelID()] if (m && m.image) return m.image else return Images.generic } getTextDescription(){ //return `<img src="https://www.victronenergy.com/_next/image?url=https%3A%2F%2Fwww.victronenergy.com%2Fupload%2Fproducts%2FSmartShunt%2520500_nw.png&w=1080&q=70"" height="150" align=”top” ></img>` return `To get the encryption key for your device, follow the instructions <a href=https://communityarchive.victronenergy.com/questions/187303/victron-bluetooth-advertising-protocol.html target="_victron_encrypt">here</a>` } prepareConfig(config){ super.prepareConfig(config) config.params.modelID=this.getModelID() } } module.exports=VictronSensor