Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ This custom component for [Home Assistant](https://www.home-assistant.io) passiv
- Kegtron
- KKM
- Mikrotik
- Minew (E9)
- Moat
- Oras
- Oral-B
Expand Down
5 changes: 5 additions & 0 deletions custom_components/ble_monitor/ble_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
from .laica import parse_laica
from .michelin import parse_michelin_tms
from .mikrotik import parse_mikrotik
from .minew import parse_minew
from .miscale import parse_miscale
from .moat import parse_moat
from .mocreo import parse_mocreo
Expand Down Expand Up @@ -327,6 +328,10 @@ def parse_advertisement(
# UUID16 = Google (used by Ruuvitag V2/V4)
sensor_data = parse_ruuvitag(self, service_data, mac)
break
elif uuid16 == 0xFFE1:
# UUID16 = Minew
sensor_data = parse_minew(self, service_data, mac)
break
elif uuid16 == 0xFEE0:
# UUID16 = Anhui Huami Information Technology Co., Ltd. (Amazfit)
if man_spec_data_list:
Expand Down
47 changes: 47 additions & 0 deletions custom_components/ble_monitor/ble_parser/minew.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Parser for Minew BLE advertisements"""
from __future__ import annotations

import logging
from struct import unpack
from typing import Any

from .helpers import to_mac, to_unformatted_mac

_LOGGER = logging.getLogger(__name__)

MINEW_FRAME_TYPE = 0xA1
E9_TEMPERATURE_VERSION = 0x13
E9_TEMPERATURE_LENGTH = 15


def parse_minew(self, data: bytes, mac: bytes) -> dict[str, Any] | None:
"""Parser for Minew sensor beacons.

Minew sends several frames under frame type 0xA1 (info, acceleration, ...),
told apart by the version byte after it. Only the E9 temperature frame (0x13)
is supported; its temperature is signed 8.8 fixed-point in °C.

Frame layout as decoded by reelyActive's advlib-ble-services (MIT):
https://github.com/reelyactive/advlib-ble-services/blob/master/lib/minew.js
"""
if (
len(data) == E9_TEMPERATURE_LENGTH
and data[4] == MINEW_FRAME_TYPE
and data[5] == E9_TEMPERATURE_VERSION
):
(battery, temp) = unpack(">Bh", data[6:9])
return {
"mac": to_unformatted_mac(mac),
"type": "E9",
"firmware": "Minew",
"temperature": round(temp / 256, 2),
"battery": battery,
"data": True,
}
if self.report_unknown == "Minew":
_LOGGER.info(
"BLE ADV from UNKNOWN Minew DEVICE: MAC: %s, ADV: %s",
to_mac(mac),
data.hex(),
)
return None
3 changes: 3 additions & 0 deletions custom_components/ble_monitor/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -2232,6 +2232,7 @@ class BLEMonitorBinarySensorEntityDescription(
'Air Mentor Pro 2' : [["temperature", "temperature calibrated", "humidity", "co2", "tvoc", "aqi", "air quality", "pm2.5", "pm10", "rssi"], [], []],
'Air Mentor 2S' : [["temperature", "temperature calibrated", "humidity", "co2", "tvoc", "aqi", "formaldehyde", "air quality", "pm2.5", "pm10", "rssi"], [], []],
'bluSensor Mini' : [["temperature", "humidity", "co2", "tvoc", "aqi", "rssi"], [], []],
'E9' : [["temperature", "battery", "rssi"], [], []],
'Meter TH S1' : [["temperature", "humidity", "battery", "rssi"], [], []],
'Meter TH plus' : [["temperature", "humidity", "battery", "rssi"], [], []],
'Laica Smart Scale' : [["weight", "impedance", "rssi"], [], []],
Expand Down Expand Up @@ -2392,6 +2393,7 @@ class BLEMonitorBinarySensorEntityDescription(
'Air Mentor Pro 2' : 'Air Mentor',
'Air Mentor 2S' : 'Air Mentor',
'bluSensor Mini' : 'Almendo',
'E9' : 'Minew',
'Meter TH S1' : 'Switchbot',
'Meter TH plus' : 'Switchbot',
'Laica Smart Scale' : 'Laica',
Expand Down Expand Up @@ -2645,6 +2647,7 @@ class BLEMonitorBinarySensorEntityDescription(
"Mi Band",
"Mi Scale",
"Mikrotik",
"Minew",
"Moat",
"MOCREO",
"Oras",
Expand Down
45 changes: 45 additions & 0 deletions custom_components/ble_monitor/test/test_minew_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""The tests for the Minew ble_parser."""
from ble_monitor.ble_parser import BleParser


class TestMinew:
"""Tests for the Minew E9 parser.

Frames are taken from the reelyActive advlib-ble-services unit tests.
"""
def test_minew_e9(self):
"""Test Minew E9 temperature frame parser."""
data_string = "043E2202010301AABBCCDDEEFF160201060303E1FF0E16E1FFA113631973AABBCCDDEEFFC4"
data = bytes(bytearray.fromhex(data_string))
# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg["firmware"] == "Minew"
assert sensor_msg["type"] == "E9"
assert sensor_msg["mac"] == "FFEEDDCCBBAA"
assert sensor_msg["data"]
assert sensor_msg["temperature"] == 25.45
assert sensor_msg["battery"] == 99
assert sensor_msg["rssi"] == -60

def test_minew_e9_negative_temperature(self):
"""Test Minew E9 temperature frame parser below 0 °C."""
data_string = "043E2202010301AABBCCDDEEFF160201060303E1FF0E16E1FFA11363FB80AABBCCDDEEFFC4"
data = bytes(bytearray.fromhex(data_string))
# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg["type"] == "E9"
assert sensor_msg["temperature"] == -4.5

def test_minew_other_frame_ignored(self):
"""Test that other Minew frames of the same length (here TVOC) are not parsed."""
data_string = "043E2202010301AABBCCDDEEFF160201060303E1FF0E16E1FFA112634000AABBCCDDEEFFC4"
data = bytes(bytearray.fromhex(data_string))
# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg is None
18 changes: 18 additions & 0 deletions docs/_devices/Minew_E9.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
manufacturer: Minew
name: Temperature Beacon
model: E9
image:
physical_description: Waterproof beacon
broadcasted_properties:
- temperature
- battery
- rssi
broadcasted_property_notes:
broadcast_rate:
active_scan:
encryption_key:
custom_firmware:
notes:
- Only the Minew temperature frame (service UUID `0xFFE1`, frame `0xA1 0x13`) is supported.
---