Support for Inkbird BLE BBQ Thermometer (#72)
* first version for inkbird ibbq * fix codestyle and move to 2-spaces-indents * battery level and online status all data as single mqtt message
This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
worker for inkbird ibbq and other equivalent cooking/BBQ thermometers.
|
||||
|
||||
Thermometer sends every ~2sec the current temperature.
|
||||
|
||||
"""
|
||||
import struct
|
||||
from bluepy import btle
|
||||
|
||||
from mqtt import MqttMessage
|
||||
from workers.base import BaseWorker
|
||||
import logger
|
||||
import json
|
||||
_LOGGER = logger.get(__name__)
|
||||
|
||||
REQUIREMENTS = ['bluepy']
|
||||
|
||||
|
||||
class IbbqWorker(BaseWorker):
|
||||
|
||||
def __init__(self, command_timeout, **args):
|
||||
self.command_timeout = command_timeout
|
||||
for arg, value in args.items():
|
||||
setattr(self, arg, value)
|
||||
self._setup()
|
||||
|
||||
def _setup(self):
|
||||
_LOGGER.info("Adding %d %s devices", len(self.devices), repr(self))
|
||||
for name, mac in self.devices.items():
|
||||
_LOGGER.info("Adding %s device '%s' (%s)", repr(self), name, mac)
|
||||
self.devices[name] = ibbqThermometer(mac, timeout=self.command_timeout)
|
||||
|
||||
def format_static_topic(self, *args):
|
||||
return '/'.join([self.topic_prefix, *args])
|
||||
|
||||
def __repr__(self):
|
||||
return self.__module__.split(".")[-1]
|
||||
|
||||
def status_update(self):
|
||||
for name, ibbq in self.devices.items():
|
||||
ret = dict()
|
||||
value=list()
|
||||
if not ibbq.connected:
|
||||
ibbq.device = ibbq.connect()
|
||||
ibbq.subscribe()
|
||||
bat, value = None, value
|
||||
else:
|
||||
bat, value = ibbq.update()
|
||||
n = 0
|
||||
ret['available'] = ibbq.connected
|
||||
ret['battery_level'] = bat
|
||||
for i in value:
|
||||
n += 1
|
||||
ret['Temp{}'.format(n)] = i
|
||||
return([MqttMessage(topic=self.format_static_topic(name), payload = json.dumps(ret))])
|
||||
|
||||
|
||||
class ibbqThermometer():
|
||||
SettingResult = "fff1"
|
||||
AccountAndVerify = 'fff2'
|
||||
RealTimeData = 'fff4'
|
||||
SettingData = 'fff5'
|
||||
Notify = b'\x01\x00'
|
||||
realTimeDataEnable = bytearray([0x0B, 0x01, 0x00, 0x00, 0x00, 0x00])
|
||||
batteryLevel = bytearray([0x08, 0x24, 0x00, 0x00, 0x00, 0x00])
|
||||
KEY = bytearray([0x21, 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01,
|
||||
0xb8, 0x22, 0x00, 0x00, 0x00, 0x00, 0x00])
|
||||
|
||||
|
||||
def getBattery(self):
|
||||
self.Setting_uuid.write(self.batteryLevel)
|
||||
|
||||
def connect(self, timeout=5):
|
||||
try:
|
||||
device =(btle.Peripheral(self.mac))
|
||||
_LOGGER.debug("%s connected ", self.mac)
|
||||
return device
|
||||
except btle.BTLEDisconnectError as er:
|
||||
_LOGGER.debug("failed connect %s", er)
|
||||
|
||||
def __init__(self, mac, timeout=5):
|
||||
self.cnt = 0
|
||||
self.batteryPct = 0
|
||||
self.timeout = timeout
|
||||
self.mac = mac
|
||||
self.values = list()
|
||||
self.device = self.connect()
|
||||
self.offline = 0
|
||||
if not self.device:
|
||||
return
|
||||
self.device = self.subscribe()
|
||||
|
||||
@property
|
||||
def connected(self):
|
||||
return(bool(self.device))
|
||||
|
||||
def subscribe(self, timeout=5):
|
||||
if self.device is None:
|
||||
return
|
||||
try:
|
||||
services = self.device.getServices()
|
||||
for service in services:
|
||||
if "fff0" not in str(service.uuid):
|
||||
continue
|
||||
for schar in service.getCharacteristics():
|
||||
if self.AccountAndVerify in str(schar.uuid):
|
||||
self.account_uuid = schar
|
||||
if self.RealTimeData in str(schar.uuid):
|
||||
self.RT_uuid = schar
|
||||
if self.SettingData in str(schar.uuid):
|
||||
self.Setting_uuid = schar
|
||||
if self.SettingResult in str(schar.uuid):
|
||||
self.SettingResult_uuid = schar
|
||||
|
||||
self.account_uuid.write(self.KEY)
|
||||
_LOGGER.info("Authenticated %s", self.mac)
|
||||
self.RT_uuid.getDescriptors()
|
||||
self.device.writeCharacteristic(self.RT_uuid.getHandle() + 1, self.Notify)
|
||||
self.device.writeCharacteristic(self.SettingResult_uuid.getHandle() +1, self.Notify)
|
||||
self.getBattery()
|
||||
self.Setting_uuid.write(self.realTimeDataEnable)
|
||||
self.device.withDelegate(MyDelegate(self))
|
||||
_LOGGER.info("Subscribed %s", self.mac)
|
||||
self.offline = 0
|
||||
except btle.BTLEException as ex:
|
||||
_LOGGER.info("failed %s %s", self.mac, ex)
|
||||
self.device = None
|
||||
_LOGGER.info("unsubscribe")
|
||||
return self.device
|
||||
|
||||
def update(self):
|
||||
if not self.connected:
|
||||
return list()
|
||||
self.values = list()
|
||||
self.cnt += 1
|
||||
try:
|
||||
if self.cnt >5:
|
||||
self.cnt = 0
|
||||
self.getBattery()
|
||||
while self.device.waitForNotifications(1):
|
||||
pass
|
||||
if self.values:
|
||||
self.offline = 0
|
||||
else:
|
||||
_LOGGER.debug("%s is silent", self.mac)
|
||||
if self.offline > 3:
|
||||
try:
|
||||
self.device.disconnect()
|
||||
except btle.BTLEInternalError as e:
|
||||
_LOGGER.debug("%s", e)
|
||||
self.device = None
|
||||
_LOGGER.debug("%s reconnect", self.mac)
|
||||
else:
|
||||
self.offline += 1
|
||||
except btle.BTLEDisconnectError as e:
|
||||
_LOGGER.debug("%s", e)
|
||||
self.device = None
|
||||
finally:
|
||||
return(self.batteryPct, self.values)
|
||||
|
||||
class MyDelegate(btle.DefaultDelegate):
|
||||
|
||||
def __init__(self, caller):
|
||||
btle.DefaultDelegate.__init__(self)
|
||||
self.caller = caller
|
||||
_LOGGER.debug("init mydelegate")
|
||||
|
||||
def handleNotification(self, cHandle, data):
|
||||
batMin = 0.95
|
||||
batMax = 1.5
|
||||
result = list()
|
||||
# safe = data
|
||||
if cHandle == 37:
|
||||
if data[0] == 0x24:
|
||||
currentV = struct.unpack('<H', data[1:3])
|
||||
maxV = struct.unpack('<H', data[3:5])
|
||||
self.caller.batteryPct = int(100* ((batMax *currentV[0]/maxV[0] -batMin)/(batMax-batMin)))
|
||||
else:
|
||||
while (len(data) > 0):
|
||||
v, data = data[0:2], data[2:]
|
||||
result.append(struct.unpack('<H', v)[0]/10)
|
||||
self.caller.values = result
|
||||
# _LOGGER.debug("called handler %s %s", cHandle, safe)
|
||||
Reference in New Issue
Block a user