bt-sensors-plugin-sk 1.1.0-beta.2.1.3 → 1.1.0-beta.2.1.3.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bt-sensors-plugin-sk",
3
- "version": "1.1.0-beta.2.1.3",
3
+ "version": "1.1.0-beta.2.1.3.2",
4
4
  "description": "Bluetooth Sensors for Signalk -- support for Victron devices, RuuviTag, Xiaomi, ATC and Inkbird",
5
5
  "main": "index.js",
6
6
  "dependencies": {
@@ -47,7 +47,7 @@ class ATC extends BTSensor{
47
47
  this.addMetadatum('batteryStrength', 'ratio', 'sensor battery strength',
48
48
  (buff)=>{return ((buff.readUInt8(9))/100)})
49
49
  this.addMetadatum('temp','K', 'temperature',
50
- (buff)=>{return parseFloat((273.15+(buff.readInt16BE(6))/100).toFixed(2))})
50
+ (buff)=>{return parseFloat((273.15+(buff.readInt16BE(6))/10).toFixed(2))})
51
51
  this.addMetadatum('humidity','ratio', 'humidity',
52
52
  (buff)=>{return ((buff.readUInt8(8))/100)})
53
53
 
@@ -0,0 +1,326 @@
1
+ /*
2
+ """Parser for Gmopeka_iot BLE advertisements.
3
+
4
+ Thanks to https://github.com/spbrogan/mopeka_pro_check for
5
+ help decoding the advertisements.
6
+
7
+ MIT License applies.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import logging
13
+ from dataclasses import dataclass
14
+ from bluetooth_data_tools import short_address
15
+ from bluetooth_sensor_state_data import BluetoothData
16
+ from home_assistant_bluetooth import BluetoothServiceInfo
17
+ from sensor_state_data import (
18
+ BinarySensorDeviceClass,
19
+ SensorDeviceClass,
20
+ SensorLibrary,
21
+ Units,
22
+ )
23
+
24
+ from .models import MediumType
25
+
26
+ _LOGGER = logging.getLogger(__name__)
27
+
28
+
29
+ # converting sensor value to height
30
+ MOPEKA_TANK_LEVEL_COEFFICIENTS = {
31
+ MediumType.PROPANE: (0.573045, -0.002822, -0.00000535),
32
+ MediumType.AIR: (0.153096, 0.000327, -0.000000294),
33
+ MediumType.FRESH_WATER: (0.600592, 0.003124, -0.00001368),
34
+ MediumType.WASTE_WATER: (0.600592, 0.003124, -0.00001368),
35
+ MediumType.LIVE_WELL: (0.600592, 0.003124, -0.00001368),
36
+ MediumType.BLACK_WATER: (0.600592, 0.003124, -0.00001368),
37
+ MediumType.RAW_WATER: (0.600592, 0.003124, -0.00001368),
38
+ MediumType.GASOLINE: (0.7373417462, -0.001978229885, 0.00000202162),
39
+ MediumType.DIESEL: (0.7373417462, -0.001978229885, 0.00000202162),
40
+ MediumType.LNG: (0.7373417462, -0.001978229885, 0.00000202162),
41
+ MediumType.OIL: (0.7373417462, -0.001978229885, 0.00000202162),
42
+ MediumType.HYDRAULIC_OIL: (0.7373417462, -0.001978229885, 0.00000202162),
43
+ }
44
+
45
+ MOPEKA_MANUFACTURER = 89
46
+ MOKPEKA_PRO_SERVICE_UUID = "0000fee5-0000-1000-8000-00805f9b34fb"
47
+
48
+
49
+ @dataclass
50
+ class MopekaDevice:
51
+ model: str
52
+ name: str
53
+ adv_length: int
54
+
55
+
56
+ DEVICE_TYPES = {
57
+ 0x3: MopekaDevice("M1017", "Pro Check", 10),
58
+ 0x4: MopekaDevice("Pro-200", "Pro-200", 10),
59
+ 0x5: MopekaDevice("Pro H20", "Pro Check H2O", 10),
60
+ 0x6: MopekaDevice("M1017", "Lippert BottleCheck", 10),
61
+ 0x8: MopekaDevice("M1015", "Pro Plus", 10),
62
+ 0x9: MopekaDevice("M1015", "Pro Plus with Cellular", 10),
63
+ 0xA: MopekaDevice("TD40/TD200", "TD40/TD200", 10),
64
+ 0xB: MopekaDevice("TD40/TD200", "TD40/TD200 with Cellular", 10),
65
+ 0xC: MopekaDevice("M1017", "Pro Check Universal", 10),
66
+ }
67
+
68
+
69
+ def hex(data: bytes) -> str:
70
+ """Return a string object containing two hexadecimal digits for each byte in the instance."""
71
+ return "b'{}'".format("".join(f"\\x{b:02x}" for b in data)) # noqa: E231
72
+
73
+
74
+ def battery_to_voltage(battery: int) -> float:
75
+ """Convert battery value to voltage"""
76
+ return battery / 32.0
77
+
78
+
79
+ def battery_to_percentage(battery: int) -> float:
80
+ """Convert battery value to percentage."""
81
+ return round(max(0, min(100, (((battery / 32.0) - 2.2) / 0.65) * 100)), 1)
82
+
83
+
84
+ def temp_to_celsius(temp: int) -> int:
85
+ """Convert temperature value to celsius."""
86
+ return temp - 40
87
+
88
+
89
+ def tank_level_to_mm(tank_level: int) -> int:
90
+ """Convert tank level value to mm."""
91
+ return tank_level * 10
92
+
93
+
94
+ def tank_level_and_temp_to_mm(
95
+ tank_level: int, temp: int, medium: MediumType = MediumType.PROPANE
96
+ ) -> int:
97
+ """Get the tank level in mm for a given fluid type."""
98
+ coefs = MOPEKA_TANK_LEVEL_COEFFICIENTS[medium]
99
+ return int(tank_level * (coefs[0] + (coefs[1] * temp) + (coefs[2] * (temp**2))))
100
+
101
+
102
+ class MopekaIOTBluetoothDeviceData(BluetoothData):
103
+ """Data for Mopeka IOT BLE sensors."""
104
+
105
+ def __init__(self, medium_type: MediumType = MediumType.PROPANE) -> None:
106
+ super().__init__()
107
+ self._medium_type = medium_type
108
+
109
+ def _start_update(self, service_info: BluetoothServiceInfo) -> None:
110
+ """Update from BLE advertisement data."""
111
+ _LOGGER.debug(
112
+ "Parsing Mopeka IOT BLE advertisement data: %s, MediumType is: %s",
113
+ service_info,
114
+ self._medium_type,
115
+ )
116
+ manufacturer_data = service_info.manufacturer_data
117
+ service_uuids = service_info.service_uuids
118
+ address = service_info.address
119
+ if (
120
+ MOPEKA_MANUFACTURER not in manufacturer_data
121
+ or MOKPEKA_PRO_SERVICE_UUID not in service_uuids
122
+ ):
123
+ _LOGGER.debug("Not a Mopeka IOT BLE advertisement: %s", service_info)
124
+ return
125
+ data = manufacturer_data[MOPEKA_MANUFACTURER]
126
+ model_num = data[0]
127
+ if not (device_type := DEVICE_TYPES.get(model_num)):
128
+ _LOGGER.debug("Unsupported Mopeka IOT BLE advertisement: %s", service_info)
129
+ return
130
+ adv_length = device_type.adv_length
131
+ if len(data) != adv_length:
132
+ return
133
+
134
+ self.set_device_manufacturer("Mopeka IOT")
135
+ self.set_device_type(device_type.model)
136
+ self.set_device_name(f"{device_type.name} {short_address(address)}")
137
+ battery = data[1]
138
+ battery_voltage = battery_to_voltage(battery)
139
+ battery_percentage = battery_to_percentage(battery)
140
+ button_pressed = bool(data[2] & 0x80 > 0)
141
+ temp = data[2] & 0x7F
142
+ temp_celsius = temp_to_celsius(temp)
143
+ tank_level = ((int(data[4]) << 8) + data[3]) & 0x3FFF
144
+ tank_level_mm = tank_level_and_temp_to_mm(tank_level, temp, self._medium_type)
145
+ reading_quality = data[4] >> 6
146
+ accelerometer_x = data[8]
147
+ accelerometer_y = data[9]
148
+
149
+ self.update_predefined_sensor(SensorLibrary.TEMPERATURE__CELSIUS, temp_celsius)
150
+ self.update_predefined_sensor(
151
+ SensorLibrary.BATTERY__PERCENTAGE, battery_percentage
152
+ )
153
+ self.update_predefined_sensor(
154
+ SensorLibrary.VOLTAGE__ELECTRIC_POTENTIAL_VOLT,
155
+ battery_voltage,
156
+ name="Battery Voltage",
157
+ key="battery_voltage",
158
+ )
159
+ self.update_predefined_binary_sensor(
160
+ BinarySensorDeviceClass.OCCUPANCY,
161
+ button_pressed,
162
+ key="button_pressed",
163
+ name="Button pressed",
164
+ )
165
+ self.update_sensor(
166
+ "tank_level",
167
+ Units.LENGTH_MILLIMETERS,
168
+ tank_level_mm if reading_quality >= 1 else None,
169
+ SensorDeviceClass.DISTANCE,
170
+ "Tank Level",
171
+ )
172
+ self.update_sensor(
173
+ "accelerometer_x",
174
+ None,
175
+ accelerometer_x,
176
+ None,
177
+ "Position X",
178
+ )
179
+ self.update_sensor(
180
+ "accelerometer_y",
181
+ None,
182
+ accelerometer_y,
183
+ None,
184
+ "Position Y",
185
+ )
186
+ self.update_sensor(
187
+ "reading_quality_raw",
188
+ None,
189
+ reading_quality,
190
+ None,
191
+ "Reading quality raw",
192
+ )
193
+ self.update_sensor(
194
+ "reading_quality",
195
+ Units.PERCENTAGE,
196
+ round(reading_quality / 3 * 100),
197
+ None,
198
+ "Reading quality",
199
+ )
200
+ # Reading stars = (3-reading_quality) * "★" + (reading_quality * "⭐")
201
+ */
202
+
203
+ class MopekaDevice{
204
+ constructor (ID, name, lengthOfAd = 10){
205
+ this.ID=ID
206
+ this.name=name
207
+ this.lengthOfAd=lengthOfAd
208
+ }
209
+ }
210
+ MopekaDevices = new Map()
211
+ MopekaDevices.set()
212
+ .set (0x0, new MopekaDevice("XXXX","Unknown Mopeka device"))
213
+ .set (0x3, new MopekaDevice("M1017", "Pro Check"))
214
+ .set (0x4, new MopekaDevice("Pro-200", "Pro-200"))
215
+ .set (0x5, new MopekaDevice("Pro H20", "Pro Check H2O"))
216
+ .set (0x6, new MopekaDevice("M1017", "Lippert BottleCheck"))
217
+ .set (0x8, new MopekaDevice("M1015", "Pro Plus"))
218
+ .set (0x9, new MopekaDevice("M1015", "Pro Plus with Cellular"))
219
+ .set (0xA, new MopekaDevice("TD40/TD200", "TD40/TD200"))
220
+ .set (0xB, new MopekaDevice("TD40/TD200", "TD40/TD200 with Cellular"))
221
+ .set (0xC, new MopekaDevice("M1017", "Pro Check Universal"))
222
+
223
+ const Media={
224
+ PROPANE: {coefficients: [0.573045, -0.002822, -0.00000535]},
225
+ AIR: {coefficients: [0.153096, 0.000327, -0.000000294]},
226
+ FRESH_WATER: {coefficients: [0.600592, 0.003124, -0.00001368]},
227
+ WASTE_WATER: {coefficients: [0.600592, 0.003124, -0.00001368]},
228
+ LIVE_WELL: {coefficients: [0.600592, 0.003124, -0.00001368]},
229
+ BLACK_WATER: {coefficients: [0.600592, 0.003124, -0.00001368]},
230
+ RAW_WATER: {coefficients: [0.600592, 0.003124, -0.00001368]},
231
+ GASOLINE: {coefficients: [0.7373417462, -0.001978229885, 0.00000202162]},
232
+ DIESEL: {coefficients: [0.7373417462, -0.001978229885, 0.00000202162]},
233
+ LNG: {coefficients: [0.7373417462, -0.001978229885, 0.00000202162]},
234
+ OIL: {coefficients: [0.7373417462, -0.001978229885, 0.00000202162]},
235
+ HYDRAULIC_OIL: {coefficients: [0.7373417462, -0.001978229885, 0.00000202162]}
236
+ }
237
+
238
+
239
+ const BTSensor = require("../BTSensor");
240
+ class MopekaTankSensor extends BTSensor{
241
+ static serviceID = "0000fee5-0000-1000-8000-00805f9b34fb"
242
+ static serviceID16 = 0xFEE5
243
+
244
+ static manufacturerID = 0x0059
245
+ static async identify(device){
246
+ try{
247
+ if (await this.getManufacturerID(device)==this.manufacturerID ){
248
+ const uuids = await this.getDeviceProp(device, 'UUIDs')
249
+ if (uuids != null && uuids.length>0) {
250
+ if (uuids.includes(this.serviceID))
251
+ return this
252
+ }
253
+ }
254
+
255
+ } catch (e){
256
+ this.debug.log(e)
257
+ }
258
+ return null
259
+ }
260
+
261
+ async init(){
262
+ await super.init()
263
+ const md = this.valueIfVariant(this.getManufacturerData(this.constructor.manufacturerID))
264
+ this.modelID = md[0]
265
+ this.initMetadata()
266
+
267
+ }
268
+
269
+ getMedium(){
270
+ return Media[this?.medium??'PROPANE']
271
+ }
272
+
273
+ _tankLevel( rawLevel ){
274
+ const coefs= this.getMedium().coefficients
275
+ return rawLevel * (coefs[0] + (coefs[1] * this.temp) + (coefs[2] * (this.temp^2)))
276
+ }
277
+
278
+ initMetadata(){
279
+ const md = this.addMetadatum("medium","","type of liquid in tank")
280
+ md.isParam=true
281
+ md.enum=Object.keys(Media)
282
+
283
+ this.addMetadatum("battVolt","V","sensor battery in volts",
284
+ ((buffer)=>{
285
+ this.battVolt = (buffer.readUInt8(1)/32)
286
+ return this.battVolt
287
+ }).bind(this)
288
+ )
289
+ this.addMetadatum("battStrength","ratio","sensor battery strength",
290
+ (buffer)=>{ return Math.max(0, Math.min(100, (((this.battVolt / 32.0) - 2.2) / 0.65))) }
291
+ )
292
+ this.addMetadatum("temp","K","temperature",
293
+ ((buffer)=>{
294
+ this.temp = parseFloat(((buffer.readUInt8(2)&0x7F)+233.15).toFixed(2))
295
+ return this.temp
296
+ }).bind(this)
297
+ )
298
+ this.addMetadatum("tankLevel","m","tank level",
299
+ (buffer)=>{ return this._tankLevel(((buffer.readUInt16LE(3))&0x3FFF)*10)/1000}
300
+ )
301
+ this.addMetadatum("readingQuality","","quality of read",
302
+ (buffer)=>{ return buffer.readUInt8(4)>>6}
303
+ )
304
+ this.addMetadatum("accX","Mg","acceleration on X-axis",
305
+ (buffer)=>{ return buffer.readUInt8(8)}
306
+ )
307
+ this.addMetadatum("accY","Mg","acceleration on Y-axis",
308
+ (buffer)=>{ return buffer.readUInt8(9)}
309
+ )
310
+ }
311
+
312
+ propertiesChanged(props){
313
+ super.propertiesChanged(props)
314
+ if (props.ManufacturerData)
315
+ this.emitValuesFrom( this.getManufacturerData(this.constructor.manufacturerID) )
316
+ }
317
+ getName(){
318
+ if (this.name)
319
+ return this.name
320
+
321
+ const _name = MopekaDevices.get(this?.modelID??0x0).name
322
+ return _name?_name:MopekaDevices.get(0x0).name
323
+
324
+ }
325
+ }
326
+ module.exports=MopekaTankSensor
@@ -0,0 +1,85 @@
1
+ const BTSensor = require("../BTSensor");
2
+ class UltrasonicWindMeter extends BTSensor{
3
+ static async identify(device){
4
+ try{
5
+ const uuids = await this.getDeviceProp(device,'UUIDs')
6
+ const name = await this.getDeviceProp(device,"Name")
7
+ if (name == 'ULTRASONIC'){
8
+ return this
9
+ }
10
+ } catch (e){
11
+ this.debug(e)
12
+ return null
13
+ }
14
+ return null
15
+ }
16
+ hasGATT(){
17
+ return true
18
+ }
19
+ emitGatt(){
20
+ this.battCharacteristic.readValue()
21
+ .then((buffer)=>
22
+ this.emitData("batt", buffer)
23
+ )
24
+ this.awsCharacteristic.readValue()
25
+ .then((buffer)=>
26
+ this.emit("aws", buffer)
27
+
28
+ )
29
+ this.awaCharacteristic.readValue()
30
+ .then((buffer)=>
31
+ this.emit("awa", buffer)
32
+ )
33
+
34
+ }
35
+
36
+ initGATTConnection(){
37
+ return new Promise((resolve,reject )=>{ this.device.connect().then(async ()=>{
38
+ if (!this.gattServer) {
39
+ this.gattServer = await this.device.gatt()
40
+ this.battService = await this.gattServer.getPrimaryService("0000180f-0000-1000-8000-00805f9b34fb")
41
+ this.battCharacteristic = await this.battService.getCharacteristic("00002a19-0000-1000-8000-00805f9b34fb")
42
+ this.envService = await this.gattServer.getPrimaryService("0000181a-0000-1000-8000-00805f9b34fb")
43
+ this.awsCharacteristic = await this.envService.getCharacteristic("00002a72-0000-1000-8000-00805f9b34fb")
44
+ this.awaCharacteristic = await this.envService.getCharacteristic("00002a73-0000-1000-8000-00805f9b34fb") }
45
+ resolve(this)
46
+ }) .catch((e)=>{ reject(e.message) }) })
47
+ }
48
+ initGATTNotifications() {
49
+ Promise.resolve(this.battCharacteristic.startNotifications().then(()=>{
50
+ this.battCharacteristic.on('valuechanged', buffer => {
51
+ this.emitData("batt",buffer)
52
+ })
53
+ }))
54
+ Promise.resolve(this.awaCharacteristic.startNotifications().then(()=>{
55
+ this.awaCharacteristic.on('valuechanged', buffer => {
56
+ this.emitData("awa", buffer)
57
+ })
58
+ }))
59
+ Promise.resolve(this.awsCharacteristic.startNotifications().then(()=>{
60
+ this.awsCharacteristic.on('valuechanged', buffer => {
61
+ this.emitData("aws", buffer)
62
+ })
63
+ }))
64
+ }
65
+
66
+ async stopListening(){
67
+ super.stopListening()
68
+ if (this.battCharacteristic && await this.battCharacteristic.isNotifying()) {
69
+ await this.battCharacteristic.stopNotifications()
70
+ this.battCharacteristic=null
71
+ }
72
+ if (this.awaCharacteristic && await this.awaCharacteristic.isNotifying()) {
73
+ await this.awaCharacteristic.stopNotifications()
74
+ this.awaCharacteristic=null
75
+ }
76
+ if (this.awsCharacteristic && await this.awsCharacteristic.isNotifying()) {
77
+ await this.awsCharacteristic.stopNotifications()
78
+ this.awsCharacteristic=null
79
+ }
80
+ if (await this.device.isConnected()){
81
+ await this.device.disconnect()
82
+ }
83
+ }
84
+ }
85
+ module.exports=UltrasonicWindMeter
@@ -18,7 +18,7 @@ const BLACKLISTED = require("../BlackListedDevice.js");
18
18
  const md = await this.getDeviceProp(device,'ManufacturerData')
19
19
  if (!md) return null
20
20
  const data = md[0x2e1]
21
- if (data.value[0]==0x2) { //VE.Smart is on
21
+ if (data && data.value[0]==0x2) { //VE.Smart is on
22
22
  return BLACKLISTED
23
23
  }
24
24
  if (data && data.value[0]==0x10 && data.value[4]==mode)