1
- # Parser for iNode BLE advertisements
1
+ """ Parser for iNode BLE advertisements"""
2
2
import logging
3
+ import math
3
4
from struct import unpack
4
5
5
6
_LOGGER = logging .getLogger (__name__ )
6
7
8
+ INODE_CARE_SENSORS_IDS = {
9
+ 0x82 : "iNode Energy Meter" ,
10
+ 0x91 : "iNode Care Sensor 1" ,
11
+ 0x92 : "iNode Care Sensor 2" ,
12
+ 0x93 : "iNode Care Sensor 3" ,
13
+ 0x94 : "iNode Care Sensor 4" ,
14
+ 0x95 : "iNode Care Sensor 5" ,
15
+ 0x96 : "iNode Care Sensor 6" ,
16
+ 0x9A : "iNode Care Sensor T" ,
17
+ 0x9B : "iNode Care Sensor HT" ,
18
+ 0x9C : "iNode Care Sensor PT" ,
19
+ 0x9D : "iNode Care Sensor PHT"
20
+ }
21
+
22
+ MEASUREMENTS = {
23
+ 0x91 : ["position" , "temperature" ],
24
+ 0x92 : ["position" , "temperature" ],
25
+ 0x93 : ["position" , "temperature" , "humidity" ],
26
+ 0x94 : ["position" , "temperature" ],
27
+ 0x95 : ["position" , "temperature" , "magnetic field" ],
28
+ 0x96 : ["position" , "temperature" ],
29
+ 0x9A : ["temperature" ],
30
+ 0x9B : ["temperature" , "humidity" ],
31
+ 0x9C : ["pressure" , "temperature" ],
32
+ 0x9D : ["pressure" , "temperature" , "humidity" ],
33
+ }
34
+
7
35
8
36
def parse_inode (self , data , source_mac , rssi ):
9
- # check for adstruc length
37
+ """iNode parser"""
10
38
msg_length = len (data )
11
39
firmware = "iNode"
12
40
inode_mac = source_mac
13
41
device_id = data [3 ]
14
42
xvalue = data [4 :]
15
43
result = {"firmware" : firmware }
44
+ # Advertisement structure information https://docs.google.com/document/d/1hcBpZ1RSgHRL6wu4SlTq2bvtKSL5_sFjXMu_HRyWZiQ
16
45
if msg_length == 15 and device_id == 0x82 :
17
- device_type = " iNode Energy Meter"
18
- (rawAvg , rawSum , options , batteryAndLight , weekDayData ) = unpack ("<HIHBH" , xvalue )
46
+ """ iNode Energy Meter"" "
47
+ (raw_avg , raw_sum , options , battery_light , week_day_data ) = unpack ("<HIHBH" , xvalue )
19
48
# Average of previous minute (avg) and sum (sum)
20
49
unit = (options >> 14 ) & 3
21
50
constant = options & 0x3FFF
22
51
if unit == 0 :
23
- powerUnit = "W"
24
- energyUnit = "kWh"
52
+ power_unit = "W"
53
+ energy_unit = "kWh"
25
54
constant = constant if constant > 0 else 100
26
55
elif unit == 1 :
27
- powerUnit = "m3"
28
- energyUnit = "m3"
56
+ power_unit = "m3"
57
+ energy_unit = "m3"
29
58
constant = constant if constant > 0 else 1000
30
59
else :
31
- powerUnit = "cnt"
32
- energyUnit = "cnt"
60
+ power_unit = "cnt"
61
+ energy_unit = "cnt"
33
62
constant = constant if constant > 0 else 1
34
- power = 1000 * 60 * rawAvg / constant
35
- energy = rawSum / constant
63
+ power = 1000 * 60 * raw_avg / constant
64
+ energy = raw_sum / constant
36
65
37
66
# Battery in % and voltage level in V
38
- battery = (batteryAndLight >> 4 ) & 0x0F
67
+ battery = (battery_light >> 4 ) & 0x0F
39
68
if battery == 1 :
40
- batteryLevel = 100
69
+ battery_level = 100
41
70
else :
42
- batteryLevel = 10 * (min (battery , 11 ) - 1 )
43
- batteryVoltage = (batteryLevel - 10 ) * 1.2 / 100 + 1.8
71
+ battery_level = 10 * (min (battery , 11 ) - 1 )
72
+ battery_voltage = (battery_level - 10 ) * 1.2 / 100 + 1.8
44
73
45
74
# Light level in %
46
- lightLevel = (batteryAndLight & 0x0F ) * 100 / 15
75
+ light_level = (battery_light & 0x0F ) * 100 / 15
47
76
48
77
# Previous day of the week (weekDay) and total for the previous day (weekDayTotal)
49
- weekDay = weekDayData >> 13
50
- weekDayTotal = weekDayData & 0x1FFF
78
+ week_day = week_day_data >> 13
79
+ week_day_total = week_day_data & 0x1FFF
51
80
52
81
result .update (
53
82
{
54
83
"energy" : energy ,
55
- "energy unit" : energyUnit ,
84
+ "energy unit" : energy_unit ,
56
85
"power" : power ,
57
- "power unit" : powerUnit ,
86
+ "power unit" : power_unit ,
58
87
"constant" : constant ,
59
- "battery" : batteryLevel ,
60
- "voltage" : batteryVoltage ,
61
- "light level" : lightLevel ,
62
- "week day" : weekDay ,
63
- "week day total" : weekDayTotal
88
+ "battery" : battery_level ,
89
+ "voltage" : battery_voltage ,
90
+ "light level" : light_level ,
91
+ "week day" : week_day ,
92
+ "week day total" : week_day_total
93
+ }
94
+ )
95
+ if msg_length == 26 and device_id in INODE_CARE_SENSORS_IDS :
96
+ """iNode Care Sensors"""
97
+ measurements = MEASUREMENTS [device_id ]
98
+ (
99
+ groups_battery ,
100
+ alarm ,
101
+ raw_p ,
102
+ raw_t ,
103
+ raw_h ,
104
+ raw_time1 ,
105
+ raw_time2 ,
106
+ signature
107
+ ) = unpack ("<HHHHHHHQ" , xvalue )
108
+
109
+ if "temperature" in measurements :
110
+ if device_id in [0x91 , 0x94 , 0x95 , 0x96 ]:
111
+ temp = raw_t
112
+ if temp > 127 :
113
+ temp = temp - 8192
114
+ temp = max (min (temp , 70 ), - 30 )
115
+ elif device_id in [0x92 , 0x9A ]:
116
+ msb = raw_t [0 ]
117
+ lsb = raw_t [1 ]
118
+ temp = msb * 0.0625 + 16 * (lsb & 0x0F )
119
+ if lsb & 0x10 :
120
+ temp = temp - 256
121
+ temp = max (min (temp , 70 ), - 30 )
122
+ elif device_id in [0x93 , 0x9B , 0x9D ]:
123
+ temp = (175.72 * raw_t * 4 / 65536 ) - 46.85
124
+ temp = max (min (temp , 70 ), - 30 )
125
+ elif device_id == 0x9C :
126
+ temp = 42.5 + raw_t / 480
127
+ else :
128
+ temp = 0
129
+ result .update ({"temperature" : temp })
130
+ if "humidity" in measurements :
131
+ humi = (125 * raw_h * 4 / 65536 ) - 6
132
+ humi = max (min (humi , 100 ), 1 )
133
+ result .update ({"humidity" : humi })
134
+ if "pressure" in measurements :
135
+ pressure = raw_p / 16
136
+ result .update ({"pressure" : pressure })
137
+ if "magnetic field" in measurements :
138
+ magnetic_field = raw_h
139
+ magnetic_field_direction = data [3 ] << 4
140
+ result .update ({
141
+ "magnetic field" : magnetic_field ,
142
+ "magnetic field direction" : magnetic_field_direction ,
143
+ })
144
+ if "position" in measurements :
145
+ motion = raw_p & 0x8000
146
+ acc_x = (raw_p >> 10 ) & 0x1F
147
+ acc_y = (raw_p >> 5 ) & 0x1F
148
+ acc_z = raw_p & 0x1F
149
+ # acc_x = acc_x - (acc_x & 0x10 ? 0x1F: 0)
150
+ # acc_y = acc_y - (acc_y & 0x10 ? 0x1F: 0)
151
+ # acc_z = acc_z - (acc_z & 0x10 ? 0x1F: 0)
152
+ acc = math .sqrt (acc_x ** 2 + acc_y ** 2 + acc_z ** 2 )
153
+ result .update ({
154
+ "motion" : motion ,
155
+ "acceleration" : acc ,
156
+ "acceleration x" : acc_x ,
157
+ "acceleration y" : acc_y ,
158
+ "acceleration z" : acc_z
159
+ })
160
+
161
+ # Alarm (not used in output)
162
+ move_accelerometer = alarm >> 1
163
+ level_accelerometer = alarm >> 2
164
+ level_temperature = alarm >> 3
165
+ level_humidity = alarm >> 4
166
+ contact_change = alarm >> 5
167
+ move_stopped = alarm >> 6
168
+ move_gtimer = alarm > 7
169
+ level_accelerometer_change = alarm >> 8
170
+ level_magnet_change = alarm >> 9
171
+ level_magnet_timer = alarm >> 10
172
+
173
+ # Time (not used in output)
174
+ t_1 = raw_time1 << 16
175
+ t_2 = raw_time2
176
+ time = t_1 | t_2
177
+
178
+ # Battery in % and voltage level in V
179
+ battery = (groups_battery >> 12 ) & 0x0F
180
+ if battery == 1 :
181
+ battery_level = 100
182
+ else :
183
+ battery_level = 10 * (min (battery , 11 ) - 1 )
184
+ battery_voltage = (battery_level - 10 ) * 1.2 / 100 + 1.8
185
+ result .update (
186
+ {
187
+ "battery" : battery_level ,
188
+ "voltage" : battery_voltage ,
64
189
}
65
190
)
66
191
else :
@@ -72,6 +197,7 @@ def parse_inode(self, data, source_mac, rssi):
72
197
data .hex ()
73
198
)
74
199
return None
200
+ device_type = INODE_CARE_SENSORS_IDS [device_id ]
75
201
76
202
# Check for duplicate messages
77
203
packet_id = xvalue .hex ()
@@ -103,4 +229,5 @@ def parse_inode(self, data, source_mac, rssi):
103
229
104
230
105
231
def to_mac (addr : int ):
232
+ """Return formatted MAC address"""
106
233
return ':' .join ('{:02x}' .format (x ) for x in addr ).upper ()
0 commit comments