Skip to content

Commit e95a911

Browse files
committed
Adding iNode care sensors
1 parent a415540 commit e95a911

File tree

10 files changed

+285
-97
lines changed

10 files changed

+285
-97
lines changed

custom_components/ble_monitor/__init__.py

+33-14
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
AES128KEY32_REGEX,
3232
CONF_ACTIVE_SCAN,
3333
CONF_BATT_ENTITIES,
34+
CONF_BT_AUTO_RESTART,
3435
CONF_BT_INTERFACE,
3536
CONF_DECIMALS,
3637
CONF_DEVICE_DECIMALS,
@@ -50,6 +51,7 @@
5051
CONFIG_IS_FLOW,
5152
DEFAULT_ACTIVE_SCAN,
5253
DEFAULT_BATT_ENTITIES,
54+
DEFAULT_BT_AUTO_RESTART,
5355
DEFAULT_DECIMALS,
5456
DEFAULT_DEVICE_DECIMALS,
5557
DEFAULT_DEVICE_RESTORE_STATE,
@@ -154,6 +156,9 @@
154156
vol.Optional(
155157
CONF_REPORT_UNKNOWN, default=DEFAULT_REPORT_UNKNOWN
156158
): vol.In(REPORT_UNKNOWN_LIST),
159+
vol.Optional(
160+
CONF_BT_AUTO_RESTART, default=DEFAULT_BT_AUTO_RESTART
161+
): cv.boolean,
157162
}
158163
),
159164
)
@@ -578,13 +583,20 @@ def run(self):
578583
btctrl[hci].send_scan_request(self._active)
579584
)
580585
except RuntimeError as error:
581-
_LOGGER.error(
582-
"HCIdump thread: Runtime error while sending scan request on hci%i: %s. Resetting Bluetooth adapter %s and trying again.",
583-
hci,
584-
error,
585-
BT_INTERFACES[hci],
586-
)
587-
reset_bluetooth(hci)
586+
if CONF_BT_AUTO_RESTART:
587+
_LOGGER.error(
588+
"HCIdump thread: Runtime error while sending scan request on hci%i: %s. Resetting Bluetooth adapter %s and trying again.",
589+
hci,
590+
error,
591+
BT_INTERFACES[hci],
592+
)
593+
reset_bluetooth(hci)
594+
else:
595+
_LOGGER.error(
596+
"HCIdump thread: Runtime error while sending scan request on hci%i: %s.",
597+
hci,
598+
error,
599+
)
588600
_LOGGER.debug("HCIdump thread: start main event_loop")
589601
try:
590602
self._event_loop.run_forever()
@@ -596,13 +608,20 @@ def run(self):
596608
btctrl[hci].stop_scan_request()
597609
)
598610
except RuntimeError as error:
599-
_LOGGER.error(
600-
"HCIdump thread: Runtime error while stop scan request on hci%i: %s Resetting Bluetooth adapter %s and trying again.",
601-
hci,
602-
error,
603-
BT_INTERFACES[hci],
604-
)
605-
reset_bluetooth(hci)
611+
if CONF_BT_AUTO_RESTART:
612+
_LOGGER.error(
613+
"HCIdump thread: Runtime error while stop scan request on hci%i: %s Resetting Bluetooth adapter %s and trying again.",
614+
hci,
615+
error,
616+
BT_INTERFACES[hci],
617+
)
618+
reset_bluetooth(hci)
619+
else:
620+
_LOGGER.error(
621+
"HCIdump thread: Runtime error while stop scan request on hci%i: %s.",
622+
hci,
623+
error,
624+
)
606625
except KeyError:
607626
_LOGGER.debug(
608627
"HCIdump thread: Key error while stop scan request on hci%i",

custom_components/ble_monitor/ble_parser/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,9 @@ def parse_data(self, data):
141141
elif adstruct[0] == 0x0E and adstruct[3] == 0x82: # iNode
142142
sensor_data = parse_inode(self, adstruct, mac, rssi)
143143
break
144+
elif adstruct[0] == 0x19 and adstruct[3] in [0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x9A, 0x9B, 0x9C, 0x9D]: # iNode Care Sensors
145+
sensor_data = parse_inode(self, adstruct, mac, rssi)
146+
break
144147
elif adstruct[0] == 0x15 and comp_id == 0x1000: # Moat S2
145148
sensor_data = parse_moat(self, adstruct, mac, rssi)
146149
break

custom_components/ble_monitor/ble_parser/inode.py

+153-26
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,191 @@
1-
# Parser for iNode BLE advertisements
1+
"""Parser for iNode BLE advertisements"""
22
import logging
3+
import math
34
from struct import unpack
45

56
_LOGGER = logging.getLogger(__name__)
67

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+
735

836
def parse_inode(self, data, source_mac, rssi):
9-
# check for adstruc length
37+
"""iNode parser"""
1038
msg_length = len(data)
1139
firmware = "iNode"
1240
inode_mac = source_mac
1341
device_id = data[3]
1442
xvalue = data[4:]
1543
result = {"firmware": firmware}
44+
# Advertisement structure information https://docs.google.com/document/d/1hcBpZ1RSgHRL6wu4SlTq2bvtKSL5_sFjXMu_HRyWZiQ
1645
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)
1948
# Average of previous minute (avg) and sum (sum)
2049
unit = (options >> 14) & 3
2150
constant = options & 0x3FFF
2251
if unit == 0:
23-
powerUnit = "W"
24-
energyUnit = "kWh"
52+
power_unit = "W"
53+
energy_unit = "kWh"
2554
constant = constant if constant > 0 else 100
2655
elif unit == 1:
27-
powerUnit = "m3"
28-
energyUnit = "m3"
56+
power_unit = "m3"
57+
energy_unit = "m3"
2958
constant = constant if constant > 0 else 1000
3059
else:
31-
powerUnit = "cnt"
32-
energyUnit = "cnt"
60+
power_unit = "cnt"
61+
energy_unit = "cnt"
3362
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
3665

3766
# Battery in % and voltage level in V
38-
battery = (batteryAndLight >> 4) & 0x0F
67+
battery = (battery_light >> 4) & 0x0F
3968
if battery == 1:
40-
batteryLevel = 100
69+
battery_level = 100
4170
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
4473

4574
# Light level in %
46-
lightLevel = (batteryAndLight & 0x0F) * 100 / 15
75+
light_level = (battery_light & 0x0F) * 100 / 15
4776

4877
# 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
5180

5281
result.update(
5382
{
5483
"energy": energy,
55-
"energy unit": energyUnit,
84+
"energy unit": energy_unit,
5685
"power": power,
57-
"power unit": powerUnit,
86+
"power unit": power_unit,
5887
"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,
64189
}
65190
)
66191
else:
@@ -72,6 +197,7 @@ def parse_inode(self, data, source_mac, rssi):
72197
data.hex()
73198
)
74199
return None
200+
device_type = INODE_CARE_SENSORS_IDS[device_id]
75201

76202
# Check for duplicate messages
77203
packet_id = xvalue.hex()
@@ -103,4 +229,5 @@ def parse_inode(self, data, source_mac, rssi):
103229

104230

105231
def to_mac(addr: int):
232+
"""Return formatted MAC address"""
106233
return ':'.join('{:02x}'.format(x) for x in addr).upper()

custom_components/ble_monitor/ble_parser/xiaomi.py

+1
Original file line numberDiff line numberDiff line change
@@ -877,4 +877,5 @@ def decrypt_mibeacon_legacy(self, data, i, xiaomi_mac):
877877

878878

879879
def to_mac(addr: int):
880+
"""Return formatted MAC address"""
880881
return ':'.join('{:02x}'.format(x) for x in addr).upper()

0 commit comments

Comments
 (0)