Skip to content

Commit 9c6e9ba

Browse files
authored
Merge pull request #1275 from custom-components/grundfos
Add Grundfos ALPHA Reader MI401
2 parents 5e6f359 + 08c1ee8 commit 9c6e9ba

File tree

11 files changed

+180
-8
lines changed

11 files changed

+180
-8
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ This custom component for [Home Assistant](https://www.home-assistant.io) passiv
3434
- b-parasite
3535
- Ela
3636
- Govee
37+
- Grundfos
3738
- HolyIOT
3839
- Hörmann
3940
- HHCC

custom_components/ble_monitor/ble_parser/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .bthome import parse_bthome
1515
from .const import JAALEE_TYPES, TILT_TYPES
1616
from .govee import parse_govee
17+
from .grundfos import parse_grundfos
1718
from .helpers import to_mac, to_unformatted_mac
1819
from .hhcc import parse_hhcc
1920
from .holyiot import parse_holyiot
@@ -401,6 +402,10 @@ def parse_advertisement(
401402
# Govee H5051/H5071/H5072/H5075/H5074
402403
sensor_data = parse_govee(self, man_spec_data, service_class_uuid16, local_name, mac, rssi)
403404
break
405+
elif comp_id == 0xF214 and data_len == 0x16:
406+
# Grundfos
407+
sensor_data = parse_grundfos(self, man_spec_data, mac, rssi)
408+
break
404409
elif comp_id == 0xFFFF and data_len == 0x1E:
405410
# Kegtron
406411
sensor_data = parse_kegtron(self, man_spec_data, mac, rssi)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Parser for Grundfos BLE advertisements"""
2+
import logging
3+
from struct import unpack
4+
5+
from .helpers import to_mac, to_unformatted_mac
6+
7+
_LOGGER = logging.getLogger(__name__)
8+
9+
10+
PUMP_MODE_DICT = {
11+
0: "Constant speed level 3",
12+
1: "Constant speed level 2",
13+
2: "Constant speed level 1",
14+
3: "Autoadapt",
15+
4: "Proportional pressure level 1",
16+
5: "Proportional pressure level 2",
17+
6: "Proportional pressure level 3",
18+
7: "Constant differential pressure level 1",
19+
8: "Constant differential pressure level 2",
20+
9: "Constant differential pressure level 1",
21+
}
22+
23+
24+
def parse_grundfos(self, data, source_mac, rssi):
25+
"""Grundfos parser"""
26+
device_type = "MI401"
27+
firmware = "Grundfos"
28+
grundfos_mac = source_mac
29+
30+
xvalue = data[6:17]
31+
(packet, bat_status, pump_id, flow, press, pump_mode, temp) = unpack(
32+
"<BBHHhxBB", xvalue
33+
)
34+
pump_mode = PUMP_MODE_DICT[pump_mode]
35+
36+
result = {
37+
"packet": packet,
38+
"flow": round(flow / 6.5534, 1),
39+
"water pressure": round(press / 32767, 3),
40+
"temperature": temp,
41+
"pump mode": pump_mode,
42+
"pump id": pump_id,
43+
"battery status": bat_status,
44+
}
45+
46+
if self.report_unknown == "Grundfos":
47+
_LOGGER.info(
48+
"BLE ADV from UNKNOWN Grundfos DEVICE: RSSI: %s, MAC: %s, ADV: %s",
49+
rssi,
50+
to_mac(grundfos_mac),
51+
data.hex()
52+
)
53+
54+
# check for MAC presence in sensor whitelist, if needed
55+
if self.discovery is False and grundfos_mac not in self.sensor_whitelist:
56+
_LOGGER.debug("Discovery is disabled. MAC: %s is not whitelisted!", to_mac(grundfos_mac))
57+
return None
58+
59+
result.update({
60+
"rssi": rssi,
61+
"mac": to_unformatted_mac(grundfos_mac),
62+
"type": device_type,
63+
"firmware": firmware,
64+
"data": True
65+
})
66+
return result

custom_components/ble_monitor/const.py

+38
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,17 @@ class BLEMonitorBinarySensorEntityDescription(
758758
suggested_display_precision=1,
759759
state_class=SensorStateClass.MEASUREMENT,
760760
),
761+
BLEMonitorSensorEntityDescription(
762+
key="water pressure",
763+
sensor_class="MeasuringSensor",
764+
update_behavior="Averaging",
765+
name="ble water pressure",
766+
unique_id="wp_",
767+
native_unit_of_measurement=UnitOfPressure.BAR,
768+
device_class=SensorDeviceClass.PRESSURE,
769+
suggested_display_precision=2,
770+
state_class=SensorStateClass.MEASUREMENT,
771+
),
761772
BLEMonitorSensorEntityDescription(
762773
key="conductivity",
763774
sensor_class="MeasuringSensor",
@@ -958,6 +969,18 @@ class BLEMonitorBinarySensorEntityDescription(
958969
suggested_display_precision=3,
959970
state_class=SensorStateClass.MEASUREMENT,
960971
),
972+
BLEMonitorSensorEntityDescription(
973+
key="flow",
974+
sensor_class="MeasuringSensor",
975+
update_behavior="Averaging",
976+
name="flow",
977+
unique_id="flow_",
978+
icon="mdi:wave",
979+
native_unit_of_measurement="l/hr",
980+
device_class=None,
981+
suggested_display_precision=3,
982+
state_class=SensorStateClass.MEASUREMENT,
983+
),
961984
BLEMonitorSensorEntityDescription(
962985
key="gas",
963986
sensor_class="MeasuringSensor",
@@ -1314,6 +1337,16 @@ class BLEMonitorBinarySensorEntityDescription(
13141337
native_unit_of_measurement=None,
13151338
device_class=None,
13161339
),
1340+
BLEMonitorSensorEntityDescription(
1341+
key="pump mode",
1342+
sensor_class="StateChangedSensor",
1343+
update_behavior="Instantly",
1344+
name="pump mode",
1345+
unique_id="pump_mode_",
1346+
icon="mdi:pump",
1347+
native_unit_of_measurement=None,
1348+
device_class=None,
1349+
),
13171350
BLEMonitorSensorEntityDescription(
13181351
key="timestamp",
13191352
sensor_class="StateChangedSensor",
@@ -1857,6 +1890,7 @@ class BLEMonitorBinarySensorEntityDescription(
18571890
'Amazfit Smart Scale' : 'Amazfit',
18581891
'Blustream' : 'Blustream',
18591892
'BTHome' : 'BTHome',
1893+
'MI401' : 'Grundfos',
18601894
'HHCCJCY10' : 'HHCC',
18611895
'HolyIOT BLE tracker' : 'HolyIOT',
18621896
'Supramatic E4 BS' : 'Hörmann',
@@ -1940,6 +1974,7 @@ class BLEMonitorBinarySensorEntityDescription(
19401974
"distance mm",
19411975
"duration",
19421976
"energy",
1977+
"flow",
19431978
"gas",
19441979
"gravity",
19451980
"gyroscope",
@@ -1956,6 +1991,7 @@ class BLEMonitorBinarySensorEntityDescription(
19561991
"power",
19571992
"pressure",
19581993
"pulse",
1994+
"pump mode",
19591995
"roll",
19601996
"rotation",
19611997
"speed",
@@ -1972,6 +2008,7 @@ class BLEMonitorBinarySensorEntityDescription(
19722008
"volume mL",
19732009
"volume flow rate",
19742010
"water",
2011+
"water pressure",
19752012
"weight",
19762013
]
19772014

@@ -1987,6 +2024,7 @@ class BLEMonitorBinarySensorEntityDescription(
19872024
"Blustream",
19882025
"BTHome",
19892026
"Govee",
2027+
"Grundfos",
19902028
"HHCC",
19912029
'HolyIOT',
19922030
"Hormann",

custom_components/ble_monitor/manifest.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,5 @@
1414
"btsocket>=0.2.0",
1515
"pyric>=0.1.6.3"
1616
],
17-
"version": "12.7.0"
17+
"version": "12.7.1"
1818
}

custom_components/ble_monitor/sensor.py

+12-6
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ class BaseSensor(RestoreSensor, SensorEntity):
312312
# | | |**humidity
313313
# | |**moisture
314314
# | |**pressure
315+
# | |**water pressure
315316
# | |**conductivity
316317
# | |**illuminance
317318
# | |**formaldehyde
@@ -327,11 +328,12 @@ class BaseSensor(RestoreSensor, SensorEntity):
327328
# | |**TVOC
328329
# | |**Air Quality Index
329330
# | |**UV index
330-
# | |**Volume
331-
# | |**Volume mL
332-
# | |**Volume flow rate
333-
# | |**Gas
334-
# | |**Water
331+
# | |**volume
332+
# | |**volume mL
333+
# | |**volume flow rate
334+
# | |**flow
335+
# | |**gas
336+
# | |**water
335337
# |--InstantUpdateSensor (Class)
336338
# | |**consumable
337339
# | |**heart rate
@@ -360,6 +362,7 @@ class BaseSensor(RestoreSensor, SensorEntity):
360362
# | | |**score
361363
# | | |**air quality
362364
# | | |**text
365+
# | | |**pump mode
363366
# | | |**timestamp
364367
# | |--AccelerationSensor (Class)
365368
# | | |**acceleration
@@ -864,7 +867,10 @@ def collect(self, data, period_cnt, batt_attr=None):
864867
if self.enabled is False or state == data[self.entity_description.key]:
865868
self.pending_update = False
866869
return
867-
870+
if "pump id" in data:
871+
self._extra_state_attributes["pump_id"] = data["pump id"]
872+
if "battery status" in data:
873+
self._extra_state_attributes["battery_status"] = data["battery status"]
868874
super().collect(data, period_cnt, batt_attr)
869875

870876

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""The tests for the Grundfos ble_parser."""
2+
from ble_monitor.ble_parser import BleParser
3+
4+
5+
class TestGrundfos:
6+
"""Tests for the Grundfos parser"""
7+
def test_grundfos_MI401(self):
8+
"""Test Grundfos parser for ALPHA reader MI401."""
9+
data_string = "043E2A020103009565F1164DAC1E06084D4934303116FF14F230017A03059884103E0F19070D0114FFFFFFFFC0"
10+
data = bytes(bytearray.fromhex(data_string))
11+
# pylint: disable=unused-variable
12+
ble_parser = BleParser()
13+
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)
14+
15+
assert sensor_msg["firmware"] == "Grundfos"
16+
assert sensor_msg["type"] == "MI401"
17+
assert sensor_msg["mac"] == "AC4D16F16595"
18+
assert sensor_msg["packet"] == 122
19+
assert sensor_msg["data"]
20+
assert sensor_msg["temperature"] == 13
21+
assert sensor_msg["flow"] == 645.2
22+
assert sensor_msg["water pressure"] == 0.119
23+
assert sensor_msg["pump mode"] == "Constant differential pressure level 1"
24+
assert sensor_msg["pump id"] == 38917
25+
assert sensor_msg["battery status"] == 3
26+
assert sensor_msg["rssi"] == -64

docs/_devices/Grundfos_MI401.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
manufacturer: Grundfos
3+
name: ALPHA Reader MI401
4+
model: MI401
5+
image: Grundfos_MI401.png
6+
physical_description: Rounded stick
7+
broadcasted_properties:
8+
- flow
9+
- water pressure
10+
- temperature
11+
- battery state
12+
- pump mode
13+
- pump id
14+
- rssi
15+
broadcasted_property_notes:
16+
- property: battery state
17+
note: Battery state is an attribute of the pump mode sensor. The meaning of the state is unknown. If you have more information about the
18+
meaning of the different states, let us know
19+
- property: pump id
20+
note: The pum id is an attribute of the pump mode sensor. The reader can send data of multiple pumps, each with its own id. In the current
21+
implementation, we have assumed that only one pump is connected, which means that data of multiple pumps gets mixed in the sensor output.
22+
If you have multipel pumps, modifications to the code will have to be made. Please open a new issue if you want to request multiple pump support.
23+
broadcast_rate:
24+
active_scan: true
25+
encryption_key:
26+
custom_firmware:
27+
notes:
28+
- actve scan needs to be enabled in the BLE Monitor settings for this sensor to work.
29+
---

docs/assets/images/Grundfos_MI401.png

25.8 KB
Loading

docs/config_params.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ Data from sensors with other addresses will be ignored. Default value: True
123123

124124
**Report unknown sensors**
125125

126-
(`Off`, `Acconeer`, `Air Mentor`, `Amazfit`, `ATC`, `BlueMaestro`, `Blustream`, `Brifit`, `BTHome`, `Govee`, `HolyIOT`, `Hormann`, `HHCC`, `iNode`, `iBeacon`, `Jinou`, `Kegtron`, `Mi Scale`, `Mi Band`,`Mikrotik`, `Oras`, `Qingping`, `Relsib`, `rbaron`, `Ruuvitag`, `Sensirion`, `SensorPush`, `SmartDry`, `Switchbot`, `Teltonika`, `Thermoplus`, `Xiaogui`, `Xiaomi`, `Other` or `False`)(Optional) This option is needed primarily for those who want to request an implementation of device support that is not in the list of [supported sensors](devices). If you set this parameter to one of the sensor brands, then the component will log all messages from unknown devices of the specified brand to the Home Assistant log (`logger` component must be enabled at info level, see for instructions the [FAQ](faq#my-sensor-from-the-xiaomi-ecosystem-is-not-in-the-list-of-supported-ones-how-to-request-implementation)). Using a sensor brand might not catch all BLE advertisements.
126+
(`Off`, `Acconeer`, `Air Mentor`, `Amazfit`, `ATC`, `BlueMaestro`, `Blustream`, `Brifit`, `BTHome`, `Govee`, `Grundfos`, `HolyIOT`, `Hormann`, `HHCC`, `iNode`, `iBeacon`, `Jinou`, `Kegtron`, `Mi Scale`, `Mi Band`,`Mikrotik`, `Oras`, `Qingping`, `Relsib`, `rbaron`, `Ruuvitag`, `Sensirion`, `SensorPush`, `SmartDry`, `Switchbot`, `Teltonika`, `Thermoplus`, `Xiaogui`, `Xiaomi`, `Other` or `False`)(Optional) This option is needed primarily for those who want to request an implementation of device support that is not in the list of [supported sensors](devices). If you set this parameter to one of the sensor brands, then the component will log all messages from unknown devices of the specified brand to the Home Assistant log (`logger` component must be enabled at info level, see for instructions the [FAQ](faq#my-sensor-from-the-xiaomi-ecosystem-is-not-in-the-list-of-supported-ones-how-to-request-implementation)). Using a sensor brand might not catch all BLE advertisements.
127127

128128
If you can't find the advertisements in this way, you can set this option to `Other`, which will result is all BLE advertisements being logged. You can also enable this option at device level. **Attention!** Enabling this option can lead to huge output to the Home Assistant log, especially when set to `Other`, do not enable it if you do not need it! If you know the MAC address of the sensor, its advised to set this option at device level. Details in the [FAQ](faq#my-sensor-from-the-xiaomi-ecosystem-is-not-in-the-list-of-supported-ones-how-to-request-implementation). Default value: `Off`
129129

info.md

+1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ This custom component for [Home Assistant](https://www.home-assistant.io) passiv
3636
- b-parasite
3737
- Ela
3838
- Govee
39+
- Grundfos
3940
- HolyIOT
4041
- Hörmann
4142
- HHCC

0 commit comments

Comments
 (0)