bt-sensors-plugin-sk 1.1.0-beta.2.1.4.4 → 1.1.0-beta.2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/BTSensor.js CHANGED
@@ -606,7 +606,7 @@ class BTSensor extends EventEmitter {
606
606
  NaNif(v1,v2) { return this.constructor.NaNif(v1,v2) }
607
607
 
608
608
  valueIfVariant(obj){
609
- if (obj.constructor && obj.constructor.name=='Variant')
609
+ if (obj?.constructor && obj.constructor.name=='Variant')
610
610
  return obj.value
611
611
  else
612
612
  return obj
package/README.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Bluetooth Sensors for [Signal K](http://www.signalk.org)
2
2
 
3
+ ## BETA 2.2.0
4
+
5
+ ### What's New
6
+ Support for Aranet4 environment sensor and Renogy Rover/Wanderer Controllers. Untested support for Renogy Battery and Inverter clients. If you have a Renogy Battery or Inverter with bluetooth support, please give it a try and let me know how it goes.
7
+
8
+ ### RENOGY NOTES
9
+
10
+ The class of Renogy Devices cannot be reliably identified from their Bluetooth advertisements. <br>
11
+
12
+ On the plugin config page, You will need to select the device from the Device dropdown, then select the appropriate class from the Class dropdown. After that, you will need to hit the Submit button. On restart of the plugin, the plugin should recognize the device. If you've selected the appropriate class (RenogyRoverClient for example), you should see configs for paths. <br>
13
+
3
14
 
4
15
  ## WHAT IT IS
5
16
 
package/index.js CHANGED
@@ -1,9 +1,11 @@
1
1
  const fs = require('fs')
2
2
  const util = require('util')
3
3
  const path = require('path')
4
+ const packageInfo = require("./package.json")
5
+
4
6
  const {createBluetooth} = require('node-ble')
5
7
  const {bluetooth, destroy} = createBluetooth()
6
- const packageInfo = require("./package.json")
8
+
7
9
  const BTSensor = require('./BTSensor.js')
8
10
  const BLACKLISTED = require('./sensor_classes/BlackListedDevice.js')
9
11
 
@@ -109,8 +111,17 @@ module.exports = function (app) {
109
111
  app.setPluginError(msg)
110
112
  }
111
113
  //if we're here ain't got no class for the device
112
- const sensor = new (classMap.get('UNKNOWN'))(device)
113
- await sensor.init()
114
+ var sensor
115
+ if (config.params?.sensorClass){
116
+ const c = classMap.get(config.params.sensorClass)
117
+ c.debug=app.debug
118
+ sensor = new c(device,config?.params, config?.gattParams)
119
+ sensor.debug=app.debug
120
+ await sensor.init()
121
+ } else{
122
+ sensor = new (classMap.get('UNKNOWN'))(device)
123
+ await sensor.init()
124
+ }
114
125
  return sensor
115
126
  }
116
127
 
@@ -149,7 +160,10 @@ module.exports = function (app) {
149
160
  }
150
161
 
151
162
  function loadClassMap() {
152
- classMap = utilities_sk.loadClasses(path.join(__dirname, 'sensor_classes'))
163
+ const _classMap = utilities_sk.loadClasses(path.join(__dirname, 'sensor_classes'))
164
+ classMap = new Map([..._classMap].filter(([k, v]) => !k.startsWith("_") ))
165
+ classMap.get('UNKNOWN').classMap=new Map([...classMap].filter(([k, v]) => !v.isSystem )) // share the classMap with Unknown for configuration purposes
166
+
153
167
  }
154
168
 
155
169
  app.debug(`Loading plugin ${packageInfo.version}`)
@@ -308,8 +322,10 @@ module.exports = function (app) {
308
322
  })
309
323
  .catch((e)=>{
310
324
  if (s)
311
- s.stopListening()
325
+ s.stopListening()
326
+
312
327
  app.debug(`Unable to communicate with device ${deviceNameAndAddress(config)} Reason: ${e?.message??e}`)
328
+ app.debug(e)
313
329
  reject( e?.message??e )
314
330
  })})
315
331
  }
@@ -410,11 +426,11 @@ module.exports = function (app) {
410
426
  adapter = await bluetooth.getAdapter(app.settings?.btAdapter??adapterID)
411
427
  await startScanner()
412
428
  if (starts>0){
413
- app.debug('Plugin restarting...');
429
+ app.debug(`Plugin ${packageInfo.version} restarting...`);
414
430
  if (plugin.schema.properties.peripherals.items.dependencies)
415
431
  plugin.schema.properties.peripherals.items.dependencies.mac_address.oneOf=[]
416
432
  } else {
417
- app.debug('Plugin build Beta 2.4.1.2 started');
433
+ app.debug(`Plugin ${packageInfo.version} started` )
418
434
 
419
435
  }
420
436
  starts++
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bt-sensors-plugin-sk",
3
- "version": "1.1.0-beta.2.1.4.4",
4
- "description": "Bluetooth Sensors for Signalk -- support for Victron devices, RuuviTag, Xiaomi, ATC and Inkbird, Ultrasonic, Mopeka tank reader and preliminary support for Govee GVH51xx temp sensors",
3
+ "version": "1.1.0-beta.2.2.0",
4
+ "description": "Bluetooth Sensors for Signalk -- support for Victron devices, RuuviTag, Xiaomi, ATC and Inkbird, Ultrasonic wind meters, Mopeka tank readers, Renogy Battery and Solar Controllers (new), Aranet4 environment sensors, and Govee GVH51xx temp sensors",
5
5
  "main": "index.js",
6
6
  "dependencies": {
7
7
  "dbus-next": "^0.10.2",
@@ -0,0 +1,24 @@
1
+ const BTSensor = require("../../BTSensor");
2
+ class AranetSensor extends BTSensor{
3
+ COLOR = {
4
+ 1: 'Green',
5
+ 2: 'Yellow',
6
+ 3: 'Red'
7
+ }
8
+
9
+ constructor(device, config={}){
10
+ super(device, config)
11
+ }
12
+
13
+ static async identify(device){
14
+ const md = await this.getDeviceProp(device,"ManufacturerData")
15
+
16
+ if (md && md[0x0702]) {
17
+ return this
18
+ }
19
+
20
+ return null
21
+ }
22
+
23
+ }
24
+ module.exports=AranetSensor
@@ -0,0 +1,47 @@
1
+ const AranetSensor = require("./Aranet/AranetSensor");
2
+ class Aranet2 extends AranetSensor{
3
+
4
+ constructor(device, config={}){
5
+ super(device,config)
6
+ }
7
+ static async identify(device){
8
+ const name = await this.getDeviceProp(device,"Name")
9
+
10
+ if ((await super.identify(device)!=null) &&
11
+ name.toLowerCase().startsWith("aranet2")) {
12
+ console.log("Aranet2 not currently supported")
13
+ return null //not supported for now
14
+ }
15
+ }
16
+ async init() {
17
+ await super.init()
18
+ this.initMetadata()
19
+ }
20
+
21
+ initMetadata(){
22
+ this.addMetadatum('c02', '', 'c02 concentration',
23
+ (buff)=>{return ((buff.readUInt16LE(8)))})
24
+ this.addMetadatum('temp','K', 'temperature',
25
+ (buff)=>{return parseFloat((273.15+(buff.readInt16LBE(15))/1000).toFixed(2))})
26
+
27
+ this.addMetadatum("pressure","","atmospheric pressure",
28
+ (buff)=>{return ((buff.readUInt16LE(13)))/10})
29
+ this.addMetadatum('batteryStrength', 'ratio', 'sensor battery strength',
30
+ (buff)=>{return ((buff.readUInt8(12))/100)})
31
+
32
+ this.addMetadatum('humidity','ratio', 'humidity',
33
+ (buff)=>{return ((buff.readUInt16LE(8))/10000)})
34
+
35
+ }
36
+ propertiesChanged(props){
37
+ super.propertiesChanged(props)
38
+ const buff = this.getManufacturerData(0x0702)
39
+ this.emitData("temp", buff)
40
+ this.emitData("humidity", buff)
41
+ this.emitData("pressure", buff)
42
+ this.emitData("batteryStrength", buff)
43
+
44
+ }
45
+
46
+ }
47
+ module.exports=Aranet2
@@ -0,0 +1,53 @@
1
+ const AranetSensor = require("./Aranet/AranetSensor");
2
+ class Aranet4 extends AranetSensor{
3
+
4
+ constructor(device, config={}){
5
+ super(device,config)
6
+ }
7
+
8
+ static async identify(device){
9
+ const name = await this.getDeviceProp(device,"Name")
10
+
11
+ if ((await super.identify(device) != null) &&
12
+ name.toLowerCase().startsWith("aranet4")) {
13
+ return this
14
+ }
15
+ return null
16
+ }
17
+
18
+ async init() {
19
+ await super.init()
20
+ this.initMetadata()
21
+ }
22
+
23
+ initMetadata(){
24
+ this.addMetadatum('co2', 'ppm', 'co2 concentration',
25
+ (buff)=>{return ((buff.readUInt16LE(8)))})
26
+ this.addMetadatum('temp','K', 'temperature',
27
+ (buff)=>{return parseFloat((273.15+(buff.readInt16LE(10))/20).toFixed(2))})
28
+
29
+ this.addMetadatum("pressure","hPa","atmospheric pressure",
30
+ (buff)=>{return ((buff.readUInt16LE(12)))/10})
31
+
32
+ this.addMetadatum('humidity','ratio', 'humidity',
33
+ (buff)=>{return ((buff.readUInt8(14))/100)})
34
+ this.addMetadatum('batteryStrength', 'ratio', 'sensor battery strength',
35
+ (buff)=>{return ((buff.readUInt8(15))/100)})
36
+ this.addMetadatum('color', '', 'Warning color (G Y R)',
37
+ (buff)=>{return this.COLOR[buff.readUInt8(16)]})
38
+
39
+ }
40
+ propertiesChanged(props){
41
+ super.propertiesChanged(props)
42
+ const buff = this.getManufacturerData(0x0702)
43
+ this.emitData("co2", buff)
44
+ this.emitData("temp", buff)
45
+ this.emitData("humidity", buff)
46
+ this.emitData("pressure", buff)
47
+ this.emitData("batteryStrength", buff)
48
+ this.emitData("color", buff)
49
+
50
+ }
51
+
52
+ }
53
+ module.exports=Aranet4
@@ -1,5 +1,6 @@
1
1
  const BTSensor = require("../BTSensor");
2
2
  class BLACKLISTED extends BTSensor{
3
+ static isSystem = true
3
4
  static async identify(device){
4
5
  if (await this.getManufacturerID(device)===0x004C) //apple devices use
5
6
  return this //randomised macs and clog up our list
@@ -0,0 +1,51 @@
1
+ const CRC16_LOW_BYTES = [
2
+ 0x00, 0xC0, 0xC1, 0x01, 0xC3, 0x03, 0x02, 0xC2, 0xC6, 0x06, 0x07, 0xC7, 0x05, 0xC5, 0xC4, 0x04,
3
+ 0xCC, 0x0C, 0x0D, 0xCD, 0x0F, 0xCF, 0xCE, 0x0E, 0x0A, 0xCA, 0xCB, 0x0B, 0xC9, 0x09, 0x08, 0xC8,
4
+ 0xD8, 0x18, 0x19, 0xD9, 0x1B, 0xDB, 0xDA, 0x1A, 0x1E, 0xDE, 0xDF, 0x1F, 0xDD, 0x1D, 0x1C, 0xDC,
5
+ 0x14, 0xD4, 0xD5, 0x15, 0xD7, 0x17, 0x16, 0xD6, 0xD2, 0x12, 0x13, 0xD3, 0x11, 0xD1, 0xD0, 0x10,
6
+ 0xF0, 0x30, 0x31, 0xF1, 0x33, 0xF3, 0xF2, 0x32, 0x36, 0xF6, 0xF7, 0x37, 0xF5, 0x35, 0x34, 0xF4,
7
+ 0x3C, 0xFC, 0xFD, 0x3D, 0xFF, 0x3F, 0x3E, 0xFE, 0xFA, 0x3A, 0x3B, 0xFB, 0x39, 0xF9, 0xF8, 0x38,
8
+ 0x28, 0xE8, 0xE9, 0x29, 0xEB, 0x2B, 0x2A, 0xEA, 0xEE, 0x2E, 0x2F, 0xEF, 0x2D, 0xED, 0xEC, 0x2C,
9
+ 0xE4, 0x24, 0x25, 0xE5, 0x27, 0xE7, 0xE6, 0x26, 0x22, 0xE2, 0xE3, 0x23, 0xE1, 0x21, 0x20, 0xE0,
10
+ 0xA0, 0x60, 0x61, 0xA1, 0x63, 0xA3, 0xA2, 0x62, 0x66, 0xA6, 0xA7, 0x67, 0xA5, 0x65, 0x64, 0xA4,
11
+ 0x6C, 0xAC, 0xAD, 0x6D, 0xAF, 0x6F, 0x6E, 0xAE, 0xAA, 0x6A, 0x6B, 0xAB, 0x69, 0xA9, 0xA8, 0x68,
12
+ 0x78, 0xB8, 0xB9, 0x79, 0xBB, 0x7B, 0x7A, 0xBA, 0xBE, 0x7E, 0x7F, 0xBF, 0x7D, 0xBD, 0xBC, 0x7C,
13
+ 0xB4, 0x74, 0x75, 0xB5, 0x77, 0xB7, 0xB6, 0x76, 0x72, 0xB2, 0xB3, 0x73, 0xB1, 0x71, 0x70, 0xB0,
14
+ 0x50, 0x90, 0x91, 0x51, 0x93, 0x53, 0x52, 0x92, 0x96, 0x56, 0x57, 0x97, 0x55, 0x95, 0x94, 0x54,
15
+ 0x9C, 0x5C, 0x5D, 0x9D, 0x5F, 0x9F, 0x9E, 0x5E, 0x5A, 0x9A, 0x9B, 0x5B, 0x99, 0x59, 0x58, 0x98,
16
+ 0x88, 0x48, 0x49, 0x89, 0x4B, 0x8B, 0x8A, 0x4A, 0x4E, 0x8E, 0x8F, 0x4F, 0x8D, 0x4D, 0x4C, 0x8C,
17
+ 0x44, 0x84, 0x85, 0x45, 0x87, 0x47, 0x46, 0x86, 0x82, 0x42, 0x43, 0x83, 0x41, 0x81, 0x80, 0x40
18
+ ]
19
+
20
+ const CRC16_HIGH_BYTES = [
21
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
22
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
23
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
24
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
25
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
26
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
27
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
28
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
29
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
30
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
31
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
32
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
33
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40,
34
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
35
+ 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40, 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41,
36
+ 0x00, 0xC1, 0x81, 0x40, 0x01, 0xC0, 0x80, 0x41, 0x01, 0xC0, 0x80, 0x41, 0x00, 0xC1, 0x81, 0x40
37
+ ]
38
+
39
+ // Calculate CRC-16 for Modbus
40
+ function crc16Modbus(data){
41
+ var crc_high = 0xFF
42
+ var crc_low = 0xFF
43
+
44
+ for (const byte of data){
45
+ const index = crc_high ^ byte
46
+ crc_high = crc_low ^ CRC16_HIGH_BYTES[index]
47
+ crc_low = CRC16_LOW_BYTES[index]
48
+ }
49
+ return Buffer.from([crc_high, crc_low]).readUInt16BE()
50
+ }
51
+ module.exports = crc16Modbus
@@ -0,0 +1,32 @@
1
+ RENOGY_CONSTANTS={
2
+ FUNCTION:
3
+ {
4
+ 3: "READ",
5
+ 6: "WRITE"
6
+ },
7
+
8
+ CHARGING_STATE:
9
+ {
10
+ 0: 'Deactivated',
11
+ 1: 'Activated',
12
+ 2: 'MPPT',
13
+ 3: 'Equalizing',
14
+ 4: 'Boost',
15
+ 5: 'Floating',
16
+ 6: 'Current limiting'
17
+ },
18
+
19
+ LOAD_STATE: {
20
+ 0: 'Off',
21
+ 1: 'On'
22
+ },
23
+
24
+ BATTERY_TYPE: {
25
+ 1: 'Flooded/Open',
26
+ 2: 'Sealed',
27
+ 3: 'Gel',
28
+ 4: 'Lithium',
29
+ 5: 'Custom'
30
+ }
31
+ }
32
+ module.exports=RENOGY_CONSTANTS
@@ -1,107 +1,122 @@
1
+ /*
2
+ ported from https://github.com/cyrils/renogy-bt
3
+ */
1
4
  const BTSensor = require("../../BTSensor.js");
2
5
  const VC = require('./RenogyConstants.js');
3
- function sleep(x) {
4
- return new Promise((resolve) => {
5
- setTimeout(() => {
6
- resolve(x);
7
- }, x);
8
- });
9
- }
10
- class RenogySensor extends BTSensor{
6
+ const crc16Modbus = require('./CRC.js')
7
+ class RenogySensor extends BTSensor{
11
8
 
9
+ static ALIAS_PREFIX = 'BT-TH'
10
+ static TX_SERVICE = "0000ffd0-0000-1000-8000-00805f9b34fb"
11
+ static RX_SERVICE = "0000fff0-0000-1000-8000-00805f9b34fb"
12
+ static NOTIFY_CHAR_UUID = "0000fff1-0000-1000-8000-00805f9b34fb"
13
+ static WRITE_CHAR_UUID = "0000ffd1-0000-1000-8000-00805f9b34fb"
14
+ static READ_FUNC = 3
15
+ static WRITE_FUNC = 6
12
16
  constructor(device,config,gattConfig){
13
17
  super(device,config,gattConfig)
14
- this.encryptionKey = config?.encryptionKey
15
18
  }
19
+ static async getReadWriteCharacteristics(device){
20
+ const gattServer = await device.gatt()
21
+ const txService= await gattServer.getPrimaryService(this.TX_SERVICE)
22
+ const rxService= await gattServer.getPrimaryService(this.RX_SERVICE)
23
+ const rxChar = await rxService.getCharacteristic(this.NOTIFY_CHAR_UUID)
24
+ const txChar = await txService.getCharacteristic(this.WRITE_CHAR_UUID)
25
+ return {read: rxChar, write: txChar}
26
+ }
27
+
28
+ static async sendReadFunctionRequest(writeCharacteristic, deviceID, writeReq, words){
29
+ var b = Buffer.alloc(8)
30
+ b.writeUInt8(deviceID,0)
31
+ b.writeUInt8(this.READ_FUNC,1)
32
+ b.writeUInt16BE(writeReq,2)
33
+ b.writeUInt16BE(words,4)
34
+ b.writeUInt16BE(crc16Modbus(b.subarray(0,6)),6)
35
+
36
+ await writeCharacteristic.writeValueWithResponse(b, 0)
16
37
 
17
- static async identifyMode(device, mode){
18
-
19
- var md = await this.getDeviceProp(device,'ManufacturerData')
20
- if (md==undefined || !Object.hasOwn(md,0x2e1))
21
- return null
22
- else {
23
-
24
- if (md[0x2e1].value[0]==0x10) {
25
- if (md[0x2e1].value[4]==mode)
26
- return this
27
- else
28
- return null
29
- }
30
-
31
- var hasDataPacket=false
32
- device.helper._prepare()
33
- device.helper.on("PropertiesChanged",
34
- (props)=> {
35
- if (Object.hasOwn(props,'ManufacturerData')){
36
- md = props['ManufacturerData'].value
37
- hasDataPacket=md[0x2e1].value[0]==0x10
38
- }
39
- })
40
- while (!hasDataPacket) {
41
- await sleep(500)
42
- }
43
- device.helper.removeListeners()
44
- if (md[0x2e1].value[4]==mode)
45
- return this
46
- else
47
- return null
48
- }
38
+ }
39
+ static identify(device){
40
+ return null
49
41
  }
50
42
 
51
43
  async init(){
52
44
  await super.init()
53
- }
54
- alarmReason(alarmValue){
55
- return this.constructor.AlarmReason[alarmValue]
56
- }
57
- getModelName(){
58
- return VC?.MODEL_ID_MAP[this.model_id]??this.constructor.name+" (Model ID:"+this.model_id+")"
59
- }
45
+ var md = this.addMetadatum('refreshInterval','','refresh interval')
46
+ md.isParam = true
60
47
 
61
- decrypt(data){
62
- if (!this.encryptionKey)
63
- throw Error("Unable to decrypt: no encryption key set")
48
+ md = this.addMetadatum('deviceID', '', 'ID of device')
49
+ md.isParam = true
64
50
 
65
- const encMethod = 'aes-128-ctr';
66
- const iv = data.readUInt16LE(5);
67
- const key = Buffer.from(this.encryptionKey,'hex')
68
- const ivBuffer = Buffer.alloc(16); // 128 bits = 16 bytes
69
-
70
- ivBuffer.writeUInt16LE(iv)
71
-
72
- var encData = Buffer.from([...data.slice(8)])
73
-
74
- const decipher = crypto.createDecipheriv(encMethod, key, ivBuffer)
75
-
76
- const decData=decipher.update(encData)
51
+ await this.device.connect()
52
+ const rw = await this.constructor.getReadWriteCharacteristics(this.device)
77
53
 
78
- return Buffer.from(decData)
54
+ this.readChar = rw.read
55
+ this.writeChar = rw.write
56
+ await this.readChar.startNotifications()
79
57
 
80
58
  }
59
+
60
+ emitGATT(){
61
+ }
62
+
63
+ getModelName(){
64
+ return this?.modelID??`${this.constructor.name} Unknown model`
65
+ }
66
+
81
67
  getName(){
82
- return `Victron ${this.getModelName()}`
68
+ return `Renogy ${this.getModelName()}`
83
69
  }
84
70
  propertiesChanged(props){
85
71
  super.propertiesChanged(props)
86
- if (this.usingGATT()) return
87
-
88
- try{
89
- const md = this.getManufacturerData(0x2e1)
90
- if (md && md.length && md[0]==0x10){
91
- const decData=this.decrypt(md)
92
- this.emitValuesFrom(decData)
93
- }
94
- }
95
- catch (error) {
96
- throw new Error(`Unable to read data from ${ this.getDisplayName()}: ${error}` )
97
- }
98
72
  }
99
73
 
100
- initGATTConnection(){
101
- throw new Error( "GATT Connection unimplemented for "+this.getDisplayName())
74
+ getAllEmitterFunctions(){
75
+ return []
76
+ }
77
+
78
+ getDeviceID()
79
+ {
80
+ return this?.deviceID??0xFF
81
+ }
82
+
83
+ async sendReadFunctionRequest(writeReq, words){
84
+ this.constructor.sendReadFunctionRequest(
85
+ this.writeChar, this.getDeviceID(), writeReq, words)
86
+ }
87
+
88
+ async initGATTNotifications(){
89
+ this.getAllEmitterFunctions().forEach(async (emitter)=>
90
+ await emitter()
91
+ )
92
+ this.intervalID = setInterval(()=>{
93
+ this.getAllEmitterFunctions().forEach(async (emitter)=>
94
+ await emitter()
95
+ )
96
+ }, 1000*(this?.refreshInterval??60) )
102
97
  }
103
98
 
104
-
99
+
100
+ async initGATTConnection() {
101
+ return this
102
+ }
103
+ usingGATT(){
104
+ return true
105
+ }
106
+ hasGATT(){
107
+ return false
108
+ }
109
+
110
+ async stopListening(){
111
+ super.stopListening()
112
+
113
+ await this.readChar.stopNotifications()
114
+
115
+ if (await this.device.isConnected()){
116
+ await this.device.disconnect()
117
+ this.debug(`Disconnected from ${ this.getName()}`)
118
+ }
119
+ }
105
120
 
106
121
  }
107
122
  module.exports=RenogySensor
@@ -0,0 +1,114 @@
1
+ /**
2
+
3
+ */
4
+
5
+ const RenogySensor = require("./Renogy/RenogySensor.js");
6
+ class RenogyBattery extends RenogySensor {
7
+
8
+
9
+
10
+ async init(){
11
+ await super.init()
12
+ this.numberOfCells = await this.retrieveNumberOfCells()
13
+ this.deviceID = await this.retrieveDeviceID()
14
+ this.initMetadata()
15
+ }
16
+
17
+ async getAllEmitterFunctions(){
18
+ return [this.getAndEmitBatteryInfo.bind(this),
19
+ this.getAndEmitCellTemperatures.bind(this),
20
+ this.getAndEmitCellVoltages.bind(this)]
21
+ }
22
+ initMetadata(){
23
+
24
+ this.addMetadatum('numberOfCells','', 'number of cells')
25
+ this.addMetadatum('current','A','current',
26
+ (buffer)=>{return buffer.readInt16BE(3)/100})
27
+
28
+ this.addMetadatum('voltage','V','voltage',
29
+ (buffer)=>{return buffer.readUInt16BE(5)/10})
30
+
31
+ this.addMetadatum('remainingCharge', 'Ah', 'remaining charge', //TODO: units
32
+ (buffer)=>{return buffer.readUInt32BE(7)/1000})
33
+
34
+ this.addMetadatum('capacity','Ah', 'capacity',
35
+ (buffer)=>{return buffer.readUInt32BE(11)/1000})
36
+
37
+ for (let i = 0; i++ ; i < this.numberOfCells) {
38
+ this.addMetadatum(`cellVoltage${i}`, 'V', `cell #${i} voltage`,
39
+ (buffer)=>{ return buffer.readUInt16(5+ i*2) }
40
+ )
41
+ this.addMetadatum(`cellTemp${i}`, 'K', `cell #${i} temperature`,
42
+ (buffer)=>{ return buffer.readUInt16(5+ i*2)+273.15 }
43
+ )
44
+ }
45
+ }
46
+ async initGATTConnection() {
47
+ await super.initGATTConnection()
48
+ this.emit('numberOfCells', this.numberOfCells)
49
+ }
50
+
51
+ retrieveNumberOfCells(){
52
+
53
+ return new Promise( async ( resolve, reject )=>{
54
+ await this.sendReadFunctionRequest(0x1388,0x11)
55
+
56
+ const valChanged = async (buffer) => {
57
+ resolve(buffer.readUInt16(3))
58
+ }
59
+ this.readChar.once('valuechanged', valChanged )
60
+ })
61
+ }
62
+ retrieveDeviceID(){
63
+ return new Promise( async ( resolve, reject )=>{
64
+ this.sendFunctionRequest(0x14, 0x67)
65
+
66
+ const valChanged = async (buffer) => {
67
+ resolve((buffer.readUInt8(4)))
68
+ }
69
+ this.readChar.once('valuechanged', valChanged )
70
+ })
71
+ }
72
+
73
+ getAndEmitBatteryInfo(){
74
+ return new Promise( async ( resolve, reject )=>{
75
+
76
+ await this.sendReadFunctionRequest(0x13b2, 0x6)
77
+
78
+
79
+ this.readChar.once('valuechanged', (buffer) => {
80
+ ["current", "voltage", "remainingCharge", "capacity"].forEach(tag)
81
+ this.emitData( tag, buffer )
82
+
83
+ resolve(this)
84
+ })
85
+ })
86
+ }
87
+
88
+ getAndEmitCellVoltages(){
89
+ return new Promise( async ( resolve, reject )=>{
90
+ this.sendReadFunctionRequest(0x1388,0x11)
91
+
92
+ this.readChar.once('valuechanged', (buffer) => {
93
+ for (let i = 0; i++ ; i < this.numberOfCells)
94
+ this.emitData(`cellVoltage${i}`, buffer)
95
+ resolve(this)
96
+ })
97
+ })
98
+ }
99
+
100
+ getAndEmitCellTemperatures(){
101
+ return new Promise( async ( resolve, reject )=>{
102
+ this.sendReadFunctionRequest(0x1399,0x22)
103
+
104
+ this.readChar.once('valuechanged', buffer => {
105
+ for (let i = 0; i++ ; i < this.numberOfCells)
106
+ this.emitData(`cellTemp${i}`, buffer)
107
+ resolve(this)
108
+ })
109
+ })
110
+ }
111
+
112
+
113
+ }
114
+ module.exports=RenogyBattery
@@ -0,0 +1,113 @@
1
+ /**
2
+
3
+ */
4
+
5
+ const RenogySensor = require("./Renogy/RenogySensor.js");
6
+ const RC=require("./Renogy/RenogyConstants.js")
7
+ class RenogyInverter extends RenogySensor {
8
+
9
+
10
+ async init(){
11
+ await super.init()
12
+ this.initMetadata()
13
+ }
14
+
15
+ initMetadata(){
16
+ this.addMetadatum('batteryType', '', "battery type")
17
+
18
+ this.addMetadatum('ueiVoltage','V','UEI Voltage',
19
+ (buffer)=>{return buffer.readUInt16BE(3)/10})
20
+ this.addMetadatum('ueiCurrent','V','UEI current',
21
+ (buffer)=>{return buffer.readUInt16BE(5)/10})
22
+ this.addMetadatum('voltage','V','voltage',
23
+ (buffer)=>{return buffer.readUInt16BE(7)/10})
24
+ this.addMetadatum('loadCurrent','A','load current',
25
+ (buffer)=>{return buffer.readUInt16BE(9)})
26
+ this.addMetadatum('frequency','hz','frequency',
27
+ (buffer)=>{return buffer.readUInt16BE(11)/100})
28
+ this.addMetadatum('temperature','K','temperature',
29
+ (buffer)=>{return (buffer.readUInt16BE(13)/10)+273.15})
30
+
31
+ this.addMetadatum('solarVoltage','V', 'solar voltage',
32
+ (buffer)=>{return buffer.readUInt16BE(3)/10})
33
+ this.addMetadatum('solarCurrent','A', 'solar current',
34
+ (buffer)=>{return buffer.readUInt16BE(5)/10})
35
+ this.addMetadatum('solarPower','W', 'solar power',
36
+ (buffer)=>{return buffer.readUInt16BE(7)})
37
+ this.addMetadatum('solarChargingStatus', '', 'solar charging state',
38
+ (buffer)=>{return RC.CHARGING_STATE[buffer.readUInt16BE(9)]})
39
+ this.addMetadatum('solarChargingPower', 'W', 'solar charging power',
40
+ (buffer)=>{return buffer.readUInt16BE(11)})
41
+
42
+ this.addMetadatum('loadPower','W','load power',
43
+ (buffer)=>{return buffer.readUInt16BE(3)})
44
+ this.addMetadatum('chargingCurrent','A','charging current',
45
+ (buffer)=>{return buffer.readUInt16BE(5)/10})
46
+
47
+ }
48
+
49
+ getBatteryType(){
50
+ return new Promise( async ( resolve, reject )=>{
51
+ this.sendReadFunctionRequest( 0xe004, 0x01)
52
+
53
+ const valChanged = async (buffer) => {
54
+ resolve(RC.BATTERY_TYPE[(buffer.readUInt8(4))])
55
+ }
56
+ this.readChar.once('valuechanged', valChanged )
57
+ })
58
+ }
59
+
60
+ getAndEmitInverterStats(){
61
+ return new Promise( async ( resolve, reject )=>{
62
+
63
+ await this.sendReadFunctionRequest(0xfa0, 0x8)
64
+
65
+ this.readChar.once('valuechanged', (buffer) => {
66
+ ["ueiVoltage","ueiCurrent", "voltage", "loadCurrent", "frequency","temperature"].forEach(tag)
67
+ this.emitData( tag, buffer )
68
+
69
+ resolve(this)
70
+ })
71
+ })
72
+ }
73
+
74
+ getAndEmitSolarCharging(){
75
+ return new Promise( async ( resolve, reject )=>{
76
+
77
+ await this.sendReadFunctionRequest(0x10e9, 0x5)
78
+
79
+ this.readChar.once('valuechanged', (buffer) => {
80
+ ["solarVoltage","solarCurrent", "solarPower", "solarChargingStatus", "solarChargingPower"].forEach(tag)
81
+ this.emitData( tag, buffer )
82
+
83
+ resolve(this)
84
+ })
85
+ })
86
+ }
87
+
88
+ getAndEmitInverterLoad(){
89
+ return new Promise( async ( resolve, reject )=>{
90
+
91
+ await this.sendReadFunctionRequest(0x113a, 0x2)
92
+
93
+ this.readChar.once('valuechanged', (buffer) => {
94
+ ["loadPower", "chargingCurrent"].forEach(tag)
95
+ this.emitData( tag, buffer )
96
+
97
+ resolve(this)
98
+ })
99
+ })
100
+ }
101
+ getAllEmitterFunctions(){
102
+ return [
103
+ this.getAndEmitInverterLoad.bind(this),
104
+ this.getAndEmitInverterStats.bind(this),
105
+ this.getAndEmitSolarCharging.bind(this)]
106
+ }
107
+ async initGATTConnection() {
108
+ await super.initGATTConnection()
109
+ this.batteryType = await this.getBatteryType()
110
+ this.emit('batteryType', this.batteryType)
111
+ }
112
+ }
113
+ module.exports=RenogyInverter
@@ -0,0 +1,133 @@
1
+ /**
2
+
3
+ */
4
+
5
+ const RenogySensor = require("./Renogy/RenogySensor.js");
6
+ const RC=require("./Renogy/RenogyConstants.js")
7
+ const crc16Modbus = require('./Renogy/CRC.js')
8
+
9
+ class RenogyRoverClient extends RenogySensor {
10
+
11
+ async init(){
12
+ await super.init()
13
+ this.initMetadata()
14
+ this.modelID=await this.retrieveModelID()
15
+ }
16
+
17
+ initMetadata(){
18
+ //Buffer(73) [1, 3, 68, 32, 32, 82, 78, 71, 45, 67, 84, 82, 76, 45, 87, 78, 68, 51, 48, 7, 140, 0, 132, 0, 126, 0, 120, 0, 111, 0, 106, 100, 50, 0, 5, 0, 120, 0, 120, 0, 28, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 5, 0, 5, 2, 148, 0, 5, 206, 143, 34, 228, buffer: ArrayBuffer(8192), byteLength: 73, byteOffset: 6144, length: 73, Symbol(Symbol.toStringTag): 'Uint8Array']
19
+
20
+ this.addMetadatum('batteryType', '', "battery type")
21
+ this.addMetadatum('batteryPercentage', 'ratio', "battery percentage",
22
+ (buffer)=>{return buffer.readUInt16BE(3) })
23
+ this.addMetadatum('batteryVoltage', 'V', "battery voltage",
24
+ (buffer)=>{return buffer.readUInt16BE((5))/10})
25
+ this.addMetadatum('batteryCurrent', 'A', 'battery current',
26
+ (buffer)=>{return buffer.readUInt16BE((7))/100})
27
+ this.addMetadatum('controllerTemperature', 'K', 'controller temperature',
28
+ (buffer)=>{return buffer.readInt8((9))+273.15})
29
+ this.addMetadatum('batteryTemperature', 'K', 'battery temperature',
30
+ (buffer)=>{return buffer.readInt8((10))+273.15})
31
+ this.addMetadatum('loadVoltage', 'V', 'load voltage',
32
+ (buffer)=>{return buffer.readUInt16BE((11))/10})
33
+ this.addMetadatum('loadCurrent', 'A', 'load current',
34
+ (buffer)=>{return buffer.readUInt16BE((13))/100})
35
+ this.addMetadatum('loadPower', 'W', 'load power',
36
+ (buffer)=>{return buffer.readUInt16BE((15))})
37
+ this.addMetadatum('pvVoltage', 'V', 'pv voltage',
38
+ (buffer)=>{return buffer.readUInt16BE((17))/10})
39
+ this.addMetadatum('pvCurrent', 'A', 'pv current',
40
+ (buffer)=>{return buffer.readUInt16BE((19))/100})
41
+ this.addMetadatum('pvPower', 'W', 'pv power',
42
+ (buffer)=>{return buffer.readUInt16BE(21)})
43
+ this.addMetadatum('maxChargingPowerToday', 'W', 'max charging power today',
44
+ (buffer)=>{return buffer.readUInt16BE(33)})
45
+ this.addMetadatum('maxDischargingPowerToday', 'W', 'max discharging power today',
46
+ (buffer)=>{return buffer.readUInt16BE(35)})
47
+ this.addMetadatum('chargingAmpHoursToday', 'Ah', 'charging amp hours today',
48
+ (buffer)=>{return buffer.readUInt16BE(37)})
49
+ this.addMetadatum('dischargingAmpHoursToday', 'Ah', 'discharging amp hours today',
50
+ (buffer)=>{return buffer.readUInt16BE(39)})
51
+ this.addMetadatum('powerGenerationToday', 'W', 'power generation today',
52
+ (buffer)=>{return buffer.readUInt16BE(41)})
53
+ this.addMetadatum('powerConsumptionToday', 'W', 'power consumption today',
54
+ (buffer)=>{return buffer.readUInt16BE(43)})
55
+ this.addMetadatum('powerGenerationTotal', 'W', 'power generation total',
56
+ (buffer)=>{return buffer.readUInt32BE(59)})
57
+ this.addMetadatum('loadStatus', '', 'load status',
58
+ (buffer)=>{return RC.LOAD_STATE[buffer.readUInt8(67)>>7]})
59
+
60
+ this.addMetadatum('chargingStatus', '', 'charging status',
61
+ (buffer)=>{return RC.CHARGING_STATE[buffer.readUInt8(68)]})
62
+ }
63
+
64
+ retrieveDeviceID(){
65
+ return new Promise( async ( resolve, reject )=>{
66
+ this.sendReadFunctionRequest(0x1A, 0x1)
67
+
68
+ const valChanged = async (buffer) => {
69
+ resolve((buffer.readUInt8(4)))
70
+ }
71
+ this.readChar.once('valuechanged', valChanged )
72
+ })
73
+ }
74
+
75
+ retrieveBatteryType(){
76
+ return new Promise( async ( resolve, reject )=>{
77
+ //Buffer(7) [255, 3, 2, 0, 1, 80, 80, buffer: ArrayBuffer(8192), byteLength: 7, byteOffset: 864, length: 7, Symbol(Symbol.toStringTag): 'Uint8Array']
78
+
79
+ this.sendReadFunctionRequest(0xe004, 0x01)
80
+
81
+ const valChanged = async (buffer) => {
82
+ resolve(RC.BATTERY_TYPE[(buffer.readUInt8(4))])
83
+ }
84
+ this.readChar.once('valuechanged', valChanged )
85
+ })
86
+ }
87
+
88
+
89
+ async retrieveModelID(){
90
+ return new Promise( async ( resolve, reject )=>{
91
+
92
+ await this.sendReadFunctionRequest(0x0c,0x08)
93
+
94
+ this.readChar.once('valuechanged', async (buffer) => {
95
+ if (buffer[2]!=0x10)
96
+ reject("Unknown error retrieving model ID") //???
97
+ const model = buffer.subarray(3,17).toString().trim()
98
+ resolve(model)
99
+ })
100
+ })
101
+ }
102
+ async initGATTConnection() {
103
+ await super.initGATTConnection()
104
+ this.batteryType = await this.retrieveBatteryType()
105
+ this.emit('batteryType', this.batteryType)
106
+ if (!this.deviceID)
107
+ this.deviceID = await this.retrieveDeviceID()
108
+
109
+ }
110
+
111
+
112
+ getAllEmitterFunctions(){
113
+ return [this.getAndEmitChargeInfo.bind(this)]
114
+ }
115
+
116
+ async getAndEmitChargeInfo(){
117
+ return new Promise( async ( resolve, reject )=>{
118
+ try {
119
+ this.sendReadFunctionRequest(0x100, 0x22)
120
+
121
+ this.readChar.once('valuechanged', buffer => {
122
+ this.emitValuesFrom(buffer)
123
+ resolve(this)
124
+ })
125
+
126
+ } catch (error) {
127
+ reject(error?.message??error)
128
+ }
129
+ })
130
+ }
131
+
132
+ }
133
+ module.exports=RenogyRoverClient
@@ -1,5 +1,7 @@
1
1
  const BTSensor = require("../BTSensor");
2
2
  class UNKNOWN extends BTSensor{
3
+ static isSystem = true
4
+
3
5
  static identify(device){
4
6
  return null
5
7
  }
@@ -7,6 +9,13 @@ class UNKNOWN extends BTSensor{
7
9
  await super.init()
8
10
  if (!this.currentProperties.Name)
9
11
  this.currentProperties.Name= `Unknown device from ${this.getManufacturer()}`
12
+ var md =this.addMetadatum("sensorClass",'',"Sensor Class")
13
+ md.isParam=true
14
+ md.enum=Array.from(this.constructor.classMap.keys())
15
+ //md.enumNames=this.constructor.classMap
16
+
10
17
  }
18
+
19
+
11
20
  }
12
21
  module.exports=UNKNOWN
@@ -53,7 +53,7 @@ function sleep(x) {
53
53
  await super.init()
54
54
  const md =this.addMetadatum('encryptionKey','', "Encryption Key")
55
55
  md.isParam = true
56
- this.model_id=this.getManufacturerData(0x2e1).readUInt16LE(2)
56
+ this.model_id=this.getManufacturerData(0x2e1)?.readUInt16LE(2)??"Unknown"
57
57
  }
58
58
  alarmReason(alarmValue){
59
59
  return this.constructor.AlarmReason[alarmValue]
@@ -67,7 +67,7 @@ class VictronACCharger extends VictronSensor{
67
67
  this.emit("batt3", this.NaNif(br.read_unsigned_int(13),0x1FFF)/100)
68
68
  this.emit("curr3", this.NaNif(br.read_unsigned_int(11),0x7FF)/10)
69
69
  this.emit("temp", this.NaNif(br.read_unsigned_int(7),0x7F)+233.15)
70
- this.emit("acCurr", this.NaNif(br.read_unsigned_int(9),0x1FF)/10)
70
+ this.emit("acCurr", this.NaNif(br.read_unsigned_int(9),0x1FF)/100)
71
71
  }
72
72
  }
73
73
  module.exports=VictronACCharger