""" Parser for ThermoPro BLE advertisements. This file is shamelessly copied from the following repository: https://github.com/Ernst79/bleparser/blob/c42ae922e1abed2720c7fac993777e1bd59c0c93/package/bleparser/thermopro.py MIT License applies. """ from __future__ import annotations import logging from math import tanh from struct import Struct from bluetooth_data_tools import short_address from bluetooth_sensor_state_data import BluetoothData from sensor_state_data import SensorLibrary from habluetooth import BluetoothServiceInfoBleak _LOGGER = logging.getLogger(__name__) BATTERY_VALUE_TO_LEVEL = { 0: 1, 1: 50, 2: 100, } UNPACK_TEMP_HUMID = Struct(" float: raw = 52.317286 * tanh(voltage / 273.624277936 - 8.76485439394) + 51.06925 clamped = max(0, min(raw, 100)) return round(clamped, 2) # TP97x only supply temperatures in F, even when display is set to celsius def fahrenheit_to_celsius(fahrenheit_temp: float) -> float: celsius_temp = (fahrenheit_temp - 32) * 5 / 9 return round(celsius_temp, 1) class ThermoProBluetoothDeviceData(BluetoothData): """Date update for ThermoPro Bluetooth devices.""" def _update_sensors( self, probe_one_indexed: int, internal_temp: int, ambient_temp: int, battery_percent: float, ) -> None: self.update_predefined_sensor( SensorLibrary.TEMPERATURE__CELSIUS, internal_temp, key=f"internal_temperature_probe_{probe_one_indexed}", name=f"Probe {probe_one_indexed} Internal Temperature", ) self.update_predefined_sensor( SensorLibrary.TEMPERATURE__CELSIUS, ambient_temp, key=f"ambient_temperature_probe_{probe_one_indexed}", name=f"Probe {probe_one_indexed} Ambient Temperature", ) self.set_precision(0) self.update_predefined_sensor( SensorLibrary.BATTERY__PERCENTAGE, battery_percent, key=f"battery_probe_{probe_one_indexed}", name=f"Probe {probe_one_indexed} Battery", ) def _update_sensors_tp97( self, probe_one_indexed: int, internal_temp_tip: float, internal_temp_center: float, internal_temp_end: float, ambient_temp: float, battery_percent: float, ) -> None: self.set_precision(1) self.update_predefined_sensor( SensorLibrary.TEMPERATURE__CELSIUS, internal_temp_tip, key=f"internal_temperature_probe_{probe_one_indexed}", name=f"Probe {probe_one_indexed} Internal Tip Temperature", ) self.update_predefined_sensor( SensorLibrary.TEMPERATURE__CELSIUS, internal_temp_center, key=f"internal_center_temperature_probe_{probe_one_indexed}", name=f"Probe {probe_one_indexed} Internal Center Temperature", ) self.update_predefined_sensor( SensorLibrary.TEMPERATURE__CELSIUS, internal_temp_end, key=f"internal_end_temperature_probe_{probe_one_indexed}", name=f"Probe {probe_one_indexed} Internal End Temperature", ) self.update_predefined_sensor( SensorLibrary.TEMPERATURE__CELSIUS, ambient_temp, key=f"ambient_temperature_probe_{probe_one_indexed}", name=f"Probe {probe_one_indexed} Ambient Temperature", ) self.set_precision(0) self.update_predefined_sensor( SensorLibrary.BATTERY__PERCENTAGE, battery_percent, key=f"battery_probe_{probe_one_indexed}", name=f"Probe {probe_one_indexed} Battery", ) def _start_update(self, service_info: BluetoothServiceInfoBleak) -> None: """Update from BLE advertisement data.""" _LOGGER.debug("Parsing thermopro BLE advertisement data: %s", service_info) name = service_info.name if not name.startswith(("TP35", "TP39", "TP96", "TP97")): return model = name.split(" ")[0] self.set_device_type(model) self.set_title(f"{name} {short_address(service_info.address)}") self.set_device_name(name) self.set_precision(2) self.set_device_manufacturer("ThermoPro") changed_manufacturer_data = self.changed_manufacturer_data(service_info) if not changed_manufacturer_data or len(changed_manufacturer_data) > 1: # If len(changed_manufacturer_data) > 1 it means we switched # ble adapters so we do not know which data is the latest # and we need to wait for the next update. return last_id = list(changed_manufacturer_data)[-1] data = ( int(last_id).to_bytes(2, byteorder="little") + changed_manufacturer_data[last_id] ) data_length = len(data) if data_length == 23 and name.startswith("TP97"): # TP972 has a 23-byte format # It has an internal temp probe and an ambient temp probe ( probe_zero_indexed, ambient_temp, battery_voltage, internal_tip_temp, # tip temperature internal_center_temp, # center temperature internal_end_temp, # end temperature _, # looks like a static id _, _, ) = UNPACK_SPIKE_PRO_TEMP(data) probe_one_indexed = probe_zero_indexed + 1 internal_tip_temp = fahrenheit_to_celsius(internal_tip_temp - 54) internal_center_temp = fahrenheit_to_celsius(internal_center_temp - 54) internal_end_temp = fahrenheit_to_celsius(internal_end_temp - 54) ambient_temp = int(fahrenheit_to_celsius(ambient_temp - 54)) battery_percent = tp96_battery(battery_voltage) self._update_sensors_tp97( probe_one_indexed, internal_tip_temp, internal_center_temp, internal_end_temp, ambient_temp, battery_percent, ) return if data_length in (7, 13) and name.startswith(("TP96", "TP97")): # TP96 has a different format that is shared with TP970 # It has an internal temp probe and an ambient temp probe # Some probes append 6 extra bytes (observed to be the reversed # base-station MAC address) after the standard 7-byte payload, # yielding 13 bytes total. The extra bytes are ignored. ( probe_zero_indexed, internal_temp, battery_voltage, ambient_temp, ) = UNPACK_SPIKE_TEMP(data[:7]) probe_one_indexed = probe_zero_indexed + 1 internal_temp = internal_temp - 30 ambient_temp = ambient_temp - 30 battery_percent = tp96_battery(battery_voltage) self._update_sensors( probe_one_indexed, internal_temp, ambient_temp, battery_percent ) return # TP357S, TP397 and TP393 if data_length >= 6 and name.startswith("TP3"): # battery value is represented by the lower two bits of byte 4 # (verified with TP357S on laboratory power supply) battery_value = data[4] & 3 if battery_value in BATTERY_VALUE_TO_LEVEL: self.update_predefined_sensor( SensorLibrary.BATTERY__PERCENTAGE, BATTERY_VALUE_TO_LEVEL[battery_value], ) # TP357S could report invalid temperature and humidity values # filter them out temp_humi = data[1:4] if temp_humi == b"\xff\xff\xff": return (temp, humi) = UNPACK_TEMP_HUMID(temp_humi) self.update_predefined_sensor(SensorLibrary.TEMPERATURE__CELSIUS, temp / 10) self.update_predefined_sensor(SensorLibrary.HUMIDITY__PERCENTAGE, humi) return _LOGGER.error("Error parsing data from probe: %s", data)