diff --git a/src/Xiaomi_Scale.py b/src/Xiaomi_Scale.py index e4bbecc..dfebed3 100644 --- a/src/Xiaomi_Scale.py +++ b/src/Xiaomi_Scale.py @@ -22,7 +22,9 @@ import binascii import time import os import sys +import subprocess from bluepy import btle +from bluepy.btle import Scanner, BTLEDisconnectError, BTLEManagementError, DefaultDelegate import paho.mqtt.client as mqtt from datetime import datetime @@ -38,6 +40,7 @@ MQTT_TIMEOUT = int(os.getenv('MQTT_TIMEOUT', 60)) MQTT_PREFIX = os.getenv('MQTT_PREFIX', 'miscale') TIME_INTERVAL = int(os.getenv('TIME_INTERVAL', 30)) OLD_MEASURE = '' +MQTT_CONNECTED = False # User Variables... USER1_GT = int(os.getenv('USER1_GT', '70')) # If the weight is greater than this number, we'll assume that we're weighing User #1 @@ -59,125 +62,158 @@ USER3_DOB = os.getenv('USER3_DOB', '1988-01-01') # DOB (in yyyy-mm-dd format) class ScanProcessor(): - def GetAge(self, d1): - d1 = datetime.strptime(d1, "%Y-%m-%d") - d2 = datetime.strptime(datetime.today().strftime('%Y-%m-%d'),'%Y-%m-%d') - return abs((d2 - d1).days)/365 + def GetAge(self, d1): + d1 = datetime.strptime(d1, "%Y-%m-%d") + d2 = datetime.strptime(datetime.today().strftime('%Y-%m-%d'),'%Y-%m-%d') + return abs((d2 - d1).days)/365 - def __init__(self): - self.mqtt_client = None - self.connected = False - self._start_client() + def __init__(self): + global MQTT_CONNECTED + DefaultDelegate.__init__(self) + if not MQTT_CONNECTED: + self.mqtt_client = None + self._start_client() - def handleDiscovery(self, dev, isNewDev, isNewData): - global OLD_MEASURE - if dev.addr == MISCALE_MAC.lower() and isNewDev: - for (sdid, desc, data) in dev.getScanData(): - ### Xiaomi V1 Scale ### - if data.startswith('1d18') and sdid == 22: - measunit = data[4:6] - measured = int((data[8:10] + data[6:8]), 16) * 0.01 - unit = '' - if measunit.startswith(('03', 'b3')): unit = 'lbs' - if measunit.startswith(('12', 'b2')): unit = 'jin' - if measunit.startswith(('22', 'a2')): unit = 'kg' ; measured = measured / 2 - if unit: - if OLD_MEASURE != round(measured, 2): - print('') - self._publish(round(measured, 2), unit, str(datetime.today().strftime('%Y-%m-%d-%H:%M:%S')), "", "") - OLD_MEASURE = round(measured, 2) + def handleDiscovery(self, dev, isNewDev, isNewData): + global OLD_MEASURE + if dev.addr == MISCALE_MAC.lower() and isNewDev: + for (sdid, desc, data) in dev.getScanData(): + ### Xiaomi V1 Scale ### + if data.startswith('1d18') and sdid == 22: + measunit = data[4:6] + measured = int((data[8:10] + data[6:8]), 16) * 0.01 + unit = '' + if measunit.startswith(('03', 'b3')): unit = 'lbs' + if measunit.startswith(('12', 'b2')): unit = 'jin' + if measunit.startswith(('22', 'a2')): unit = 'kg' ; measured = measured / 2 + if unit: + if OLD_MEASURE != round(measured, 2): + print('') + self._publish(round(measured, 2), unit, str(datetime.today().strftime('%Y-%m-%d-%H:%M:%S')), "", "") + OLD_MEASURE = round(measured, 2) - ### Xiaomi V2 Scale ### - if data.startswith('1b18') and sdid == 22: - data2 = bytes.fromhex(data[4:]) - ctrlByte1 = data2[1] - isStabilized = ctrlByte1 & (1<<5) - hasImpedance = ctrlByte1 & (1<<1) + ### Xiaomi V2 Scale ### + if data.startswith('1b18') and sdid == 22: + data2 = bytes.fromhex(data[4:]) + ctrlByte1 = data2[1] + isStabilized = ctrlByte1 & (1<<5) + hasImpedance = ctrlByte1 & (1<<1) - measunit = data[4:6] - measured = int((data[28:30] + data[26:28]), 16) * 0.01 - unit = '' - if measunit == "03": unit = 'lbs' - if measunit == "02": unit = 'kg' ; measured = measured / 2 - #mitdatetime = datetime.strptime(str(int((data[10:12] + data[8:10]), 16)) + " " + str(int((data[12:14]), 16)) +" "+ str(int((data[14:16]), 16)) +" "+ str(int((data[16:18]), 16)) +" "+ str(int((data[18:20]), 16)) +" "+ str(int((data[20:22]), 16)), "%Y %m %d %H %M %S") - miimpedance = str(int((data[24:26] + data[22:24]), 16)) - if unit and isStabilized: - if OLD_MEASURE != round(measured, 2) + int(miimpedance): - print('') - self._publish(round(measured, 2), unit, str(datetime.today().strftime('%Y-%m-%d-%H:%M:%S')), hasImpedance, miimpedance) - OLD_MEASURE = round(measured, 2) + int(miimpedance) + measunit = data[4:6] + measured = int((data[28:30] + data[26:28]), 16) * 0.01 + unit = '' + if measunit == "03": unit = 'lbs' + if measunit == "02": unit = 'kg' ; measured = measured / 2 + #mitdatetime = datetime.strptime(str(int((data[10:12] + data[8:10]), 16)) + " " + str(int((data[12:14]), 16)) +" "+ str(int((data[14:16]), 16)) +" "+ str(int((data[16:18]), 16)) +" "+ str(int((data[18:20]), 16)) +" "+ str(int((data[20:22]), 16)), "%Y %m %d %H %M %S") + miimpedance = str(int((data[24:26] + data[22:24]), 16)) + if unit and isStabilized: + if OLD_MEASURE != round(measured, 2) + int(miimpedance): + print('') + self._publish(round(measured, 2), unit, str(datetime.today().strftime('%Y-%m-%d-%H:%M:%S')), hasImpedance, miimpedance) + OLD_MEASURE = round(measured, 2) + int(miimpedance) - if not dev.scanData: - print ('\t(no data)') + if not dev.scanData: + print ('\t(no data)') - def _start_client(self): - self.mqtt_client = mqtt.Client() - self.mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) + def _start_client(self): + global MQTT_CONNECTED + self.mqtt_client = mqtt.Client() + self.mqtt_client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD) - def _on_connect(client, _, flags, return_code): - self.connected = True - sys.stdout.write("MQTT connection: %s\n" % mqtt.connack_string(return_code)) + def _on_connect(client, _, flags, return_code): + global MQTT_CONNECTED + MQTT_CONNECTED = True + loop_flag = 0 + sys.stdout.write("MQTT connection: %s\n" % mqtt.connack_string(return_code)) - self.mqtt_client.on_connect = _on_connect - self.mqtt_client.connect(MQTT_HOST, MQTT_PORT, MQTT_TIMEOUT) - self.mqtt_client.loop_start() + self.mqtt_client.on_connect = _on_connect + self.mqtt_client.connect(MQTT_HOST, MQTT_PORT, MQTT_TIMEOUT) + self.mqtt_client.loop_start() - def _publish(self, weight, unit, mitdatetime, hasImpedance, miimpedance): - if not self.connected: - sys.stderr.write('Not connected to MQTT server\n') - exit() - if int(weight) > USER1_GT: - user = USER1_NAME - height = USER1_HEIGHT - age = self.GetAge(USER1_DOB) - sex = USER1_SEX - elif int(weight) < USER2_LT: - user = USER2_NAME - height = USER2_HEIGHT - age = self.GetAge(USER2_DOB) - sex = USER2_SEX - else: - user = USER3_NAME - height = USER3_HEIGHT - age = self.GetAge(USER3_DOB) - sex = USER3_SEX - lib = Xiaomi_Scale_Body_Metrics.bodyMetrics(weight, height, age, sex, 0) - message = '{' - message += '"Weight":"' + "{:.2f}".format(weight) + '"' - message += ',"BMI":"' + "{:.2f}".format(lib.getBMI()) + '"' - message += ',"Basal Metabolism":"' + "{:.2f}".format(lib.getBMR()) + '"' - message += ',"Visceral Fat":"' + "{:.2f}".format(lib.getVisceralFat()) + '"' + while not MQTT_CONNECTED: # wait for MQTT connecting Ack + time.sleep(.01) - if hasImpedance: - lib = Xiaomi_Scale_Body_Metrics.bodyMetrics(weight, height, age, sex, int(miimpedance)) - message += ',"Lean Body Mass":"' + "{:.2f}".format(lib.getLBMCoefficient()) + '"' - message += ',"Body Fat":"' + "{:.2f}".format(lib.getFatPercentage()) + '"' - message += ',"Water":"' + "{:.2f}".format(lib.getWaterPercentage()) + '"' - message += ',"Bone Mass":"' + "{:.2f}".format(lib.getBoneMass()) + '"' - message += ',"Muscle Mass":"' + "{:.2f}".format(lib.getMuscleMass()) + '"' - message += ',"Protein":"' + "{:.2f}".format(lib.getProteinPercentage()) + '"' - #message += ',"Body Type":"' + str(lib.getBodyTypeScale(getBodyType())) + '"' - #message += ',"Metabolic Age":"' + str(lib.getMetabolicAge()) + '"' + def _publish(self, weight, unit, mitdatetime, hasImpedance, miimpedance): + global MQTT_CONNECTED + if not MQTT_CONNECTED: + sys.stderr.write('Not connected to MQTT server\n') + exit() + if int(weight) > USER1_GT: + user = USER1_NAME + height = USER1_HEIGHT + age = self.GetAge(USER1_DOB) + sex = USER1_SEX + elif int(weight) < USER2_LT: + user = USER2_NAME + height = USER2_HEIGHT + age = self.GetAge(USER2_DOB) + sex = USER2_SEX + else: + user = USER3_NAME + height = USER3_HEIGHT + age = self.GetAge(USER3_DOB) + sex = USER3_SEX + lib = Xiaomi_Scale_Body_Metrics.bodyMetrics(weight, height, age, sex, 0) + message = '{' + message += '"Weight":"' + "{:.2f}".format(weight) + '"' + message += ',"BMI":"' + "{:.2f}".format(lib.getBMI()) + '"' + message += ',"Basal Metabolism":"' + "{:.2f}".format(lib.getBMR()) + '"' + message += ',"Visceral Fat":"' + "{:.2f}".format(lib.getVisceralFat()) + '"' - message += ',"TimeStamp":"' + mitdatetime + '"' - message += '}' - self.mqtt_client.publish(MQTT_PREFIX + '/' + user + '/weight', message, qos=1, retain=True) - sys.stdout.write('Sent data to topic %s: %s' % (MQTT_PREFIX + '/' + user + '/weight', message + '\n')) + if hasImpedance: + lib = Xiaomi_Scale_Body_Metrics.bodyMetrics(weight, height, age, sex, int(miimpedance)) + bodyscale = ['Obese', 'Overweight', 'Thick-set', 'Lack-exerscise', 'Balanced', 'Balanced-muscular', 'Skinny', 'Balanced-skinny', 'Skinny-muscular'] + message += ',"Lean Body Mass":"' + "{:.2f}".format(lib.getLBMCoefficient()) + '"' + message += ',"Body Fat":"' + "{:.2f}".format(lib.getFatPercentage()) + '"' + message += ',"Water":"' + "{:.2f}".format(lib.getWaterPercentage()) + '"' + message += ',"Bone Mass":"' + "{:.2f}".format(lib.getBoneMass()) + '"' + message += ',"Muscle Mass":"' + "{:.2f}".format(lib.getMuscleMass()) + '"' + message += ',"Protein":"' + "{:.2f}".format(lib.getProteinPercentage()) + '"' + message += ',"Body Type":"' + str(bodyscale[lib.getBodyType()]) + '"' + message += ',"Metabolic Age":"' + "{:.0f}".format(lib.getMetabolicAge()) + '"' + + message += ',"TimeStamp":"' + mitdatetime + '"' + message += '}' + try: + self.mqtt_client.publish(MQTT_PREFIX + '/' + user + '/weight', message, qos=1, retain=True) + sys.stdout.write('Sent data to topic %s: %s' % (MQTT_PREFIX + '/' + user + '/weight', message + '\n')) + except: + sys.stdout.write('Could not publish to MQTT, Disconnecting...\n') + MQTT_CONNECTED = False + self.mqtt_client.disconnect() + pass def main(): - sys.stdout.write(' \n') - sys.stdout.write('-------------------------------------\n') - sys.stdout.write('Starting Xiaomi mi Scale...\n') - scanner = btle.Scanner().withDelegate(ScanProcessor()) - while True: - try: - scanner.scan(5, passive=True) # Adding passive=True to try and fix issues on RPi devices - except: - sys.stderr.write("Error while running the script, continuing. If you see this message too often/constantly there is probably a real issue...\n") - pass - time.sleep(TIME_INTERVAL) + sys.stdout.write(' \n') + sys.stdout.write('-------------------------------------\n') + sys.stdout.write('Starting Xiaomi mi Scale...\n') + BluetoothFailCounter = 0 + while True: + try: + scanner = btle.Scanner().withDelegate(ScanProcessor()) + scanner.scan(5) # Adding passive=True to try and fix issues on RPi devices + except BTLEDisconnectError as error: + sys.stderr.write(f"btle disconnected: {error}\n") + pass + except BTLEManagementError as error: + sys.stderr.write(f"Bluetooth connection error: {error}\n") + if BluetoothFailCounter >= 4: + sys.stderr.write(f"5+ Bluetooth connection errors. Resetting Bluetooth...\n") + cmd = 'hciconfig hci0 reset' + ps = subprocess.Popen(cmd, shell=True) + time.sleep(30) + BluetoothFailCounter = 0 + else: + BluetoothFailCounter+=1 + pass + except: + sys.stderr.write("Error while running the script, continuing...\n") + pass + else: + BluetoothFailCounter = 0 + time.sleep(TIME_INTERVAL) if __name__ == "__main__": - main() \ No newline at end of file + main() \ No newline at end of file