From 8235d01f291e34e2cbbdce9e6dd7a888297ac84e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 30 Aug 2022 17:00:43 -0500 Subject: [PATCH] feat: add support for the t201/t301 (#5) --- .flake8 | 2 +- src/sensorpro_ble/parser.py | 77 ++++--------- tests/test_parser.py | 220 ++++++++++++++++++++++++++++-------- 3 files changed, 195 insertions(+), 104 deletions(-) diff --git a/.flake8 b/.flake8 index d5108e4..8925966 100644 --- a/.flake8 +++ b/.flake8 @@ -1,3 +1,3 @@ [flake8] exclude = docs -max-line-length = 120 +max-line-length = 140 diff --git a/src/sensorpro_ble/parser.py b/src/sensorpro_ble/parser.py index c672623..1d0ed8f 100644 --- a/src/sensorpro_ble/parser.py +++ b/src/sensorpro_ble/parser.py @@ -1,40 +1,30 @@ """Parser for SensorPro BLE advertisements. This file is shamelessly copied from the following repository: -https://github.com/Ernst79/bleparser/blob/c42ae922e1abed2720c7fac993777e1bd59c0c93/package/bleparser/thermoplus.py +https://github.com/Ernst79/bleparser/blob/c42ae922e1abed2720c7fac993777e1bd59c0c93/package/bleparser/brifit.py MIT License applies. """ from __future__ import annotations import logging -from dataclasses import dataclass from struct import unpack from bluetooth_data_tools import short_address from bluetooth_sensor_state_data import BluetoothData from home_assistant_bluetooth import BluetoothServiceInfo -from sensor_state_data import BinarySensorDeviceClass, SensorLibrary +from sensor_state_data import SensorLibrary _LOGGER = logging.getLogger(__name__) -@dataclass -class SensorProDevice: - - model: str - name: str - - DEVICE_TYPES = { - 0x10: SensorProDevice("16", "Lanyard/mini hygrometer"), - 0x11: SensorProDevice("17", "Smart hygrometer"), - 0x15: SensorProDevice("21", "Smart hygrometer"), + 0x01: "T201", + 0x05: "T301", } +DEFAULT_MODEL = "T201" MFR_IDS = set(DEVICE_TYPES) -SERVICE_UUID = "0000fff0-0000-1000-8000-00805f9b34fb" - class SensorProBluetoothDeviceData(BluetoothData): """Date update for SensorPro Bluetooth devices.""" @@ -42,59 +32,32 @@ class SensorProBluetoothDeviceData(BluetoothData): def _start_update(self, service_info: BluetoothServiceInfo) -> None: """Update from BLE advertisement data.""" _LOGGER.debug("Parsing sensorpro BLE advertisement data: %s", service_info) - if SERVICE_UUID not in service_info.service_uuids: - return - if not MFR_IDS.intersection(service_info.manufacturer_data): + if 43605 not in service_info.manufacturer_data: return changed_manufacturer_data = self.changed_manufacturer_data(service_info) if not changed_manufacturer_data: return last_id = list(changed_manufacturer_data)[-1] - data = ( - int(last_id).to_bytes(2, byteorder="little") - + changed_manufacturer_data[last_id] - ) - msg_length = len(data) - if msg_length not in (20, 22): + + changed = changed_manufacturer_data[last_id] + if not changed.startswith(b"\x01\x01\xa4\xc1") and not changed.startswith( + b"\x01\x05\xa4\xc1" + ): return - device_id = data[0] - device_type = DEVICE_TYPES[device_id] - name = device_type.name + data = int(last_id).to_bytes(2, byteorder="little") + changed + device_id = data[3] + device_type = service_info.name or DEVICE_TYPES.get(device_id) or DEFAULT_MODEL + name = device_type self.set_precision(2) self.set_device_type(device_id) self.set_title(f"{name} {short_address(service_info.address)}") self.set_device_name(f"{name} {short_address(service_info.address)}") self.set_device_manufacturer("SensorPro") - self._process_update(data) - - def _process_update(self, data: bytes) -> None: - """Update from BLE advertisement data.""" - _LOGGER.debug("Parsing SensorPro BLE advertisement data: %s", data) - if len(data) != 20: - return - - button_pushed = data[3] & 0x80 - xvalue = data[10:16] - - (volt, temp, humi) = unpack("= 3000: - batt = 100 - elif volt >= 2600: - batt = 60 + (volt - 2600) * 0.1 - elif volt >= 2500: - batt = 40 + (volt - 2500) * 0.2 - elif volt >= 2450: - batt = 20 + (volt - 2450) * 0.4 - else: - batt = 0 - + xvalue = data[10:17] + (volt, temp, humi, batt) = unpack(">hHHB", xvalue) self.update_predefined_sensor(SensorLibrary.BATTERY__PERCENTAGE, batt) - self.update_predefined_sensor(SensorLibrary.TEMPERATURE__CELSIUS, temp / 16) - self.update_predefined_sensor(SensorLibrary.HUMIDITY__PERCENTAGE, humi / 16) + self.update_predefined_sensor(SensorLibrary.TEMPERATURE__CELSIUS, temp / 100) + self.update_predefined_sensor(SensorLibrary.HUMIDITY__PERCENTAGE, humi / 100) self.update_predefined_sensor( - SensorLibrary.VOLTAGE__ELECTRIC_POTENTIAL_VOLT, volt / 1000 - ) - self.update_predefined_binary_sensor( - BinarySensorDeviceClass.OCCUPANCY, bool(button_pushed) + SensorLibrary.VOLTAGE__ELECTRIC_POTENTIAL_VOLT, volt / 100 ) diff --git a/tests/test_parser.py b/tests/test_parser.py index 6e649a1..04a0101 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -1,8 +1,5 @@ from bluetooth_sensor_state_data import BluetoothServiceInfo, SensorUpdate from sensor_state_data import ( - BinarySensorDescription, - BinarySensorDeviceClass, - BinarySensorValue, DeviceKey, SensorDescription, SensorDeviceClass, @@ -18,68 +15,137 @@ def test_can_create(): SensorProBluetoothDeviceData() -MFR_20 = BluetoothServiceInfo( - name="SensorPro", +MFR_T201 = BluetoothServiceInfo( + name="T201", address="aa:bb:cc:dd:ee:ff", rssi=-60, service_data={}, manufacturer_data={ - 16: b"\x00\x00\xb0\x02\x00\x00G\xa4\xe2\x0c\x80\x01\xb6\x02J\x00\x00\x00" + 43605: b"\x01\x01\xa4\xc18.\xcan\x01\x07\n\x02\x13\x9dd\x00\x01\x01\x01\xa4\xc18.\xcan\x01\x07\n\x02\x13\x9dd\x00\x01" }, - service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + service_uuids=[], source="local", ) -MFR_22 = BluetoothServiceInfo( - name="SensorPro", +MFR_T301 = BluetoothServiceInfo( + name="T301", address="aa:bb:cc:dd:ee:ff", rssi=-60, service_data={}, - manufacturer_data={ - 21: b"\x00\x00\xf0\x05\x00\x00\xd7n\xbe\x01e\x00\x00\x00\xa7\x01\x00\x00\x00\x00" - }, - service_uuids=["0000fff0-0000-1000-8000-00805f9b34fb"], + manufacturer_data={43605: b"\x01\x05\xa4\xc18\x1aWv\x01\x07\tV\x17\x0ca\x00\x01"}, + service_uuids=[], source="local", ) - -def test_with_22_byte_update(): - parser = SensorProBluetoothDeviceData() - parser.supported(MFR_22) is True - assert parser.title == "Smart hygrometer EEFF" - - -def test_supported_set_the_title(): - parser = SensorProBluetoothDeviceData() - parser.supported(MFR_20) is True - assert parser.title == "Lanyard/mini hygrometer EEFF" +MFR_T301_NO_NAME = BluetoothServiceInfo( + name=None, + address="aa:bb:cc:dd:ee:ff", + rssi=-60, + service_data={}, + manufacturer_data={43605: b"\x01\x05\xa4\xc18\x1aWv\x01\x07\tV\x17\x0ca\x00\x01"}, + service_uuids=[], + source="local", +) -def test_20_byte_update(): +def test_t201(): parser = SensorProBluetoothDeviceData() - update = parser.update(MFR_20) + update = parser.update(MFR_T201) assert update == SensorUpdate( - title="Lanyard/mini hygrometer EEFF", + title="T201 EEFF", devices={ None: SensorDeviceInfo( - name="Lanyard/mini hygrometer EEFF", - model=16, + name="T201 EEFF", + model=1, manufacturer="SensorPro", sw_version=None, hw_version=None, ) }, entity_descriptions={ + DeviceKey(key="voltage", device_id=None): SensorDescription( + device_key=DeviceKey(key="voltage", device_id=None), + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, + ), DeviceKey(key="humidity", device_id=None): SensorDescription( device_key=DeviceKey(key="humidity", device_id=None), device_class=SensorDeviceClass.HUMIDITY, native_unit_of_measurement=Units.PERCENTAGE, ), + DeviceKey(key="signal_strength", device_id=None): SensorDescription( + device_key=DeviceKey(key="signal_strength", device_id=None), + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ), + DeviceKey(key="battery", device_id=None): SensorDescription( + device_key=DeviceKey(key="battery", device_id=None), + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=Units.PERCENTAGE, + ), + DeviceKey(key="temperature", device_id=None): SensorDescription( + device_key=DeviceKey(key="temperature", device_id=None), + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=Units.TEMP_CELSIUS, + ), + }, + entity_values={ + DeviceKey(key="voltage", device_id=None): SensorValue( + device_key=DeviceKey(key="voltage", device_id=None), + name="Voltage", + native_value=2.63, + ), + DeviceKey(key="humidity", device_id=None): SensorValue( + device_key=DeviceKey(key="humidity", device_id=None), + name="Humidity", + native_value=50.21, + ), + DeviceKey(key="signal_strength", device_id=None): SensorValue( + device_key=DeviceKey(key="signal_strength", device_id=None), + name="Signal " "Strength", + native_value=-60, + ), + DeviceKey(key="battery", device_id=None): SensorValue( + device_key=DeviceKey(key="battery", device_id=None), + name="Battery", + native_value=100, + ), + DeviceKey(key="temperature", device_id=None): SensorValue( + device_key=DeviceKey(key="temperature", device_id=None), + name="Temperature", + native_value=25.62, + ), + }, + binary_entity_descriptions={}, + binary_entity_values={}, + ) + + +def test_t301(): + parser = SensorProBluetoothDeviceData() + update = parser.update(MFR_T301) + assert update == SensorUpdate( + title="T301 EEFF", + devices={ + None: SensorDeviceInfo( + name="T301 EEFF", + model=5, + manufacturer="SensorPro", + sw_version=None, + hw_version=None, + ) + }, + entity_descriptions={ DeviceKey(key="voltage", device_id=None): SensorDescription( device_key=DeviceKey(key="voltage", device_id=None), device_class=SensorDeviceClass.VOLTAGE, native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, ), + DeviceKey(key="humidity", device_id=None): SensorDescription( + device_key=DeviceKey(key="humidity", device_id=None), + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=Units.PERCENTAGE, + ), DeviceKey(key="temperature", device_id=None): SensorDescription( device_key=DeviceKey(key="temperature", device_id=None), device_class=SensorDeviceClass.TEMPERATURE, @@ -97,25 +163,98 @@ def test_20_byte_update(): ), }, entity_values={ + DeviceKey(key="voltage", device_id=None): SensorValue( + device_key=DeviceKey(key="voltage", device_id=None), + name="Voltage", + native_value=2.63, + ), DeviceKey(key="humidity", device_id=None): SensorValue( device_key=DeviceKey(key="humidity", device_id=None), name="Humidity", - native_value=43.38, + native_value=59.0, + ), + DeviceKey(key="temperature", device_id=None): SensorValue( + device_key=DeviceKey(key="temperature", device_id=None), + name="Temperature", + native_value=23.9, + ), + DeviceKey(key="battery", device_id=None): SensorValue( + device_key=DeviceKey(key="battery", device_id=None), + name="Battery", + native_value=97, + ), + DeviceKey(key="signal_strength", device_id=None): SensorValue( + device_key=DeviceKey(key="signal_strength", device_id=None), + name="Signal " "Strength", + native_value=-60, ), + }, + binary_entity_descriptions={}, + binary_entity_values={}, + ) + + +def test_t301_passive(): + parser = SensorProBluetoothDeviceData() + update = parser.update(MFR_T301_NO_NAME) + assert update == SensorUpdate( + title="T301 EEFF", + devices={ + None: SensorDeviceInfo( + name="T301 EEFF", + model=5, + manufacturer="SensorPro", + sw_version=None, + hw_version=None, + ) + }, + entity_descriptions={ + DeviceKey(key="voltage", device_id=None): SensorDescription( + device_key=DeviceKey(key="voltage", device_id=None), + device_class=SensorDeviceClass.VOLTAGE, + native_unit_of_measurement=Units.ELECTRIC_POTENTIAL_VOLT, + ), + DeviceKey(key="humidity", device_id=None): SensorDescription( + device_key=DeviceKey(key="humidity", device_id=None), + device_class=SensorDeviceClass.HUMIDITY, + native_unit_of_measurement=Units.PERCENTAGE, + ), + DeviceKey(key="temperature", device_id=None): SensorDescription( + device_key=DeviceKey(key="temperature", device_id=None), + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement=Units.TEMP_CELSIUS, + ), + DeviceKey(key="battery", device_id=None): SensorDescription( + device_key=DeviceKey(key="battery", device_id=None), + device_class=SensorDeviceClass.BATTERY, + native_unit_of_measurement=Units.PERCENTAGE, + ), + DeviceKey(key="signal_strength", device_id=None): SensorDescription( + device_key=DeviceKey(key="signal_strength", device_id=None), + device_class=SensorDeviceClass.SIGNAL_STRENGTH, + native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + ), + }, + entity_values={ DeviceKey(key="voltage", device_id=None): SensorValue( device_key=DeviceKey(key="voltage", device_id=None), name="Voltage", - native_value=3.3, + native_value=2.63, + ), + DeviceKey(key="humidity", device_id=None): SensorValue( + device_key=DeviceKey(key="humidity", device_id=None), + name="Humidity", + native_value=59.0, ), DeviceKey(key="temperature", device_id=None): SensorValue( device_key=DeviceKey(key="temperature", device_id=None), name="Temperature", - native_value=24.0, + native_value=23.9, ), DeviceKey(key="battery", device_id=None): SensorValue( device_key=DeviceKey(key="battery", device_id=None), name="Battery", - native_value=100, + native_value=97, ), DeviceKey(key="signal_strength", device_id=None): SensorValue( device_key=DeviceKey(key="signal_strength", device_id=None), @@ -123,17 +262,6 @@ def test_20_byte_update(): native_value=-60, ), }, - binary_entity_descriptions={ - DeviceKey(key="occupancy", device_id=None): BinarySensorDescription( - device_key=DeviceKey(key="occupancy", device_id=None), - device_class=BinarySensorDeviceClass.OCCUPANCY, - ) - }, - binary_entity_values={ - DeviceKey(key="occupancy", device_id=None): BinarySensorValue( - device_key=DeviceKey(key="occupancy", device_id=None), - name="Occupancy", - native_value=False, - ) - }, + binary_entity_descriptions={}, + binary_entity_values={}, )