diff --git a/CHANGELOG.md b/mi-scale/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to mi-scale/CHANGELOG.md diff --git a/Dockerfile b/mi-scale/Dockerfile similarity index 100% rename from Dockerfile rename to mi-scale/Dockerfile diff --git a/README.md b/mi-scale/README.md similarity index 88% rename from README.md rename to mi-scale/README.md index 99db2f5..3d75afb 100644 --- a/README.md +++ b/mi-scale/README.md @@ -1,100 +1,100 @@ -# Xiaomi Mi Scale Add On for Home Assistant - -Add-On for [HomeAssistant](https://www.home-assistant.io/) to read weight measurements from Xiaomi Body Scales. - -## Supported Scales: -Name | Model | Picture ---- | --- | :---: -[Mi Smart Scale 2](https://www.mi.com/global/scale)                                                                                               | XMTZC04HM | ![Mi Scale_2](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/Mi_Smart_Scale_2_Thumb.png) -[Mi Body Composition Scale](https://www.mi.com/global/mi-body-composition-scale/) | XMTZC02HM | ![Mi Scale](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/Mi_Body_Composition_Scale_Thumb.png) -[Mi Body Composition Scale 2](https://c.mi.com/thread-2289389-1-0.html) | XMTZC05HM | ![Mi Body Composition Scale 2](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/Mi_Body_Composition_Scale_2_Thumb.png) - - -## Setup - -1. Retrieve the scale's MAC Address from the Xiaomi Mi Fit App: -![MAC Address](Screenshots/MAC_Address.png) - -2. Clone this repository -`git clone https://github.com/lolouk44/xiaomi_mi_scale_ha_add_on` - -3. Create a new directory xiaomi_mi_scale in the folder addons in your Home Assistant installation and place all files in it via SSH / Samba - -![Add-On](Screenshots/addon.png) - -4. Open Home Assistant and navigate to add-on store and clock the reload button on the top right corner. Now you should see the Xiaomi Mi Scale as a local add-on -![Add-On Store](Screenshots/addon_store.png) - -5. Install the add-on (takes a while as the container is built locally) - -6. Edit the Configuration - - -Option | Type | Required | Description ---- | --- | --- | --- -HCI_DEV | string | No | Bluetooth hci device to use. Defaults to hci0 -MISCALE_MAC | string | Yes | Mac address of your scale -MQTT_PREFIX | string | No | MQTT Topic Prefix. Defaults to miscale -MQTT_HOST | string | Yes | MQTT Server (defaults to 127.0.0.1) -MQTT_USERNAME | string | No | Username for MQTT server (comment out if not required) -MQTT_PASSWORD | string | No | Password for MQTT (comment out if not required) -MQTT_PORT | int | No | Defaults to 1883 -TIME_INTERVAL | int | No | Time in sec between each query to the scale, to allow other applications to use the Bluetooth module. Defaults to 30 -MQTT_DISCOVERY | bool | No | MQTT Discovery for Home Assistant Defaults to true -MQTT_DISCOVERY_PREFIX | string | No | MQTT Discovery Prefix for Home Assistant. Defaults to homeassistant - - -Auto-gender selection/config -- This is used to create the calculations such as BMI, Water/Bone Mass etc... -Up to 3 users possible as long as weights do not overlap! - -Option | Type | Required | Description ---- | --- | --- | --- -USER1_GT | int | Yes | If the weight is greater than this number, we'll assume that we're weighing User #1 -USER1_SEX | string | Yes | male / female -USER1_NAME | string | Yes | Name of the user -USER1_HEIGHT | int | Yes | Height (in cm) of the user -USER1_DOB | string | Yes | DOB (in yyyy-mm-dd format) -USER2_LT | int | No | If the weight is less than this number, we'll assume that we're weighing User #2 -USER2_SEX | string | No | male / female -USER2_NAME | string | No | Name of the user -USER2_HEIGHT | int | No |Height (in cm) of the user -USER2_DOB | string | No | DOB (in yyyy-mm-dd format) -USER3_SEX | string | No | male / female -USER3_NAME | string | No | Name of the user -USER3_HEIGHT | int | No |Height (in cm) of the user -USER3_DOB | string | No | DOB (in yyyy-mm-dd format) - - -7. Start the add-on - - -## Home-Assistant Setup: -Under the `sensor` block, enter as many blocks as users configured in your environment variables: - -```yaml - - platform: mqtt - name: "Example Name Weight" - state_topic: "miScale/USER_NAME/weight" - value_template: "{{ value_json['Weight'] }}" - unit_of_measurement: "kg" - json_attributes_topic: "miScale/USER_NAME/weight" - icon: mdi:scale-bathroom - - - platform: mqtt - name: "Example Name BMI" - state_topic: "miScale/USER_NAME/weight" - value_template: "{{ value_json['BMI'] }}" - icon: mdi:human-pregnant - -``` - -![Mi Scale](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/HA_Lovelace_Card.png) - -![Mi Scale](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/HA_Lovelace_Card_Details.png) - -## Acknowledgements: -Thanks to @syssi (https://gist.github.com/syssi/4108a54877406dc231d95514e538bde9) and @prototux (https://github.com/wiecosystem/Bluetooth) for their initial code - -Special thanks to [@ned-kelly](https://github.com/ned-kelly) for his help turning a "simple" python script into a fully fledged docker container - -Thanks to [@bpaulin](https://github.com/bpaulin) for his PRs and collaboration +# Xiaomi Mi Scale Add On for Home Assistant + +Add-On for [HomeAssistant](https://www.home-assistant.io/) to read weight measurements from Xiaomi Body Scales. + +## Supported Scales: +Name | Model | Picture +--- | --- | :---: +[Mi Smart Scale 2](https://www.mi.com/global/scale)                                                                                               | XMTZC04HM | ![Mi Scale_2](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/Mi_Smart_Scale_2_Thumb.png) +[Mi Body Composition Scale](https://www.mi.com/global/mi-body-composition-scale/) | XMTZC02HM | ![Mi Scale](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/Mi_Body_Composition_Scale_Thumb.png) +[Mi Body Composition Scale 2](https://c.mi.com/thread-2289389-1-0.html) | XMTZC05HM | ![Mi Body Composition Scale 2](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/Mi_Body_Composition_Scale_2_Thumb.png) + + +## Setup + +1. Retrieve the scale's MAC Address from the Xiaomi Mi Fit App: +![MAC Address](Screenshots/MAC_Address.png) + +2. Clone this repository +`git clone https://github.com/lolouk44/xiaomi_mi_scale_ha_add_on` + +3. Create a new directory xiaomi_mi_scale in the folder addons in your Home Assistant installation and place all files in it via SSH / Samba + +![Add-On](Screenshots/addon.png) + +4. Open Home Assistant and navigate to add-on store and clock the reload button on the top right corner. Now you should see the Xiaomi Mi Scale as a local add-on +![Add-On Store](Screenshots/addon_store.png) + +5. Install the add-on (takes a while as the container is built locally) + +6. Edit the Configuration + + +Option | Type | Required | Description +--- | --- | --- | --- +HCI_DEV | string | No | Bluetooth hci device to use. Defaults to hci0 +MISCALE_MAC | string | Yes | Mac address of your scale +MQTT_PREFIX | string | No | MQTT Topic Prefix. Defaults to miscale +MQTT_HOST | string | Yes | MQTT Server (defaults to 127.0.0.1) +MQTT_USERNAME | string | No | Username for MQTT server (comment out if not required) +MQTT_PASSWORD | string | No | Password for MQTT (comment out if not required) +MQTT_PORT | int | No | Defaults to 1883 +TIME_INTERVAL | int | No | Time in sec between each query to the scale, to allow other applications to use the Bluetooth module. Defaults to 30 +MQTT_DISCOVERY | bool | No | MQTT Discovery for Home Assistant Defaults to true +MQTT_DISCOVERY_PREFIX | string | No | MQTT Discovery Prefix for Home Assistant. Defaults to homeassistant + + +Auto-gender selection/config -- This is used to create the calculations such as BMI, Water/Bone Mass etc... +Up to 3 users possible as long as weights do not overlap! + +Option | Type | Required | Description +--- | --- | --- | --- +USER1_GT | int | Yes | If the weight is greater than this number, we'll assume that we're weighing User #1 +USER1_SEX | string | Yes | male / female +USER1_NAME | string | Yes | Name of the user +USER1_HEIGHT | int | Yes | Height (in cm) of the user +USER1_DOB | string | Yes | DOB (in yyyy-mm-dd format) +USER2_LT | int | No | If the weight is less than this number, we'll assume that we're weighing User #2. Defaults to USER1_GT Value +USER2_SEX | string | No | male / female. Defaults to female +USER2_NAME | string | No | Name of the user. Defaults to Serena +USER2_HEIGHT | int | No |Height (in cm) of the user. Defaults to 95 +USER2_DOB | string | No | DOB (in yyyy-mm-dd format). Defaults to 1990-01-01 +USER3_SEX | string | No | male / female. Defaults to female +USER3_NAME | string | No | Name of the user. Defaults to Missy +USER3_HEIGHT | int | No |Height (in cm) of the user. Defaults to 150 +USER3_DOB | string | No | DOB (in yyyy-mm-dd format). Defaults to 1990-01-01 + + +7. Start the add-on + + +## Home-Assistant Setup: +Under the `sensor` block, enter as many blocks as users configured in your environment variables: + +```yaml + - platform: mqtt + name: "Example Name Weight" + state_topic: "miScale/USER_NAME/weight" + value_template: "{{ value_json['Weight'] }}" + unit_of_measurement: "kg" + json_attributes_topic: "miScale/USER_NAME/weight" + icon: mdi:scale-bathroom + + - platform: mqtt + name: "Example Name BMI" + state_topic: "miScale/USER_NAME/weight" + value_template: "{{ value_json['BMI'] }}" + icon: mdi:human-pregnant + +``` + +![Mi Scale](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/HA_Lovelace_Card.png) + +![Mi Scale](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/HA_Lovelace_Card_Details.png) + +## Acknowledgements: +Thanks to @syssi (https://gist.github.com/syssi/4108a54877406dc231d95514e538bde9) and @prototux (https://github.com/wiecosystem/Bluetooth) for their initial code + +Special thanks to [@ned-kelly](https://github.com/ned-kelly) for his help turning a "simple" python script into a fully fledged docker container + +Thanks to [@bpaulin](https://github.com/bpaulin) for his PRs and collaboration diff --git a/Screenshots/MAC_Address.png b/mi-scale/Screenshots/MAC_Address.png similarity index 100% rename from Screenshots/MAC_Address.png rename to mi-scale/Screenshots/MAC_Address.png diff --git a/Screenshots/addon.png b/mi-scale/Screenshots/addon.png similarity index 100% rename from Screenshots/addon.png rename to mi-scale/Screenshots/addon.png diff --git a/Screenshots/addon_store.png b/mi-scale/Screenshots/addon_store.png similarity index 100% rename from Screenshots/addon_store.png rename to mi-scale/Screenshots/addon_store.png diff --git a/config.json b/mi-scale/config.json similarity index 81% rename from config.json rename to mi-scale/config.json index 3486664..7375333 100644 --- a/config.json +++ b/mi-scale/config.json @@ -1,74 +1,74 @@ -{ - "name": "Xiaomi Mi Scale", - "version": "0.1.6", - "slug": "xiaomi_mi_scale", - "description": "Read weight measurements from Xiamomi scale via BLE", - "url": "https://github.com/lolouk44/xiaomi_mi_scale_ha_add_on", - "image": "lolouk44/xiaomi-mi-scale", - "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], - "startup": "before", - "boot": "auto", - "panel_admin": false, - "host_network": true, - "privileged": ["NET_ADMIN", "SYS_ADMIN"], - - "options": { - "HCI_DEV": "hci0", - "MISCALE_MAC": "00:00:00:00:00:00", - "MQTT_PREFIX": "miScale", - "MQTT_HOST": "192.168.0.1", - "MQTT_USERNAME": "", - "MQTT_PASSWORD": "", - "MQTT_PORT": 1883, - "TIME_INTERVAL": 30, - "MQTT_DISCOVERY": true, - "MQTT_DISCOVERY_PREFIX": "homeassistant", - - "USER1_GT": 70, - "USER1_SEX": "male", - "USER1_NAME": "Jo", - "USER1_HEIGHT": 175, - "USER1_DOB": "1990-01-01", - - "USER2_LT": 35, - "USER2_SEX": "female", - "USER2_NAME": "Serena", - "USER2_HEIGHT": 95, - "USER2_DOB": "1990-01-01", - - "USER3_SEX": "female", - "USER3_NAME": "Missy", - "USER3_HEIGHT": 150, - "USER3_DOB": "1990-01-01" - - }, - "schema": { - "HCI_DEV": "str", - "MISCALE_MAC": "str", - "MQTT_PREFIX": "str", - "MQTT_HOST": "str", - "MQTT_USERNAME": "str", - "MQTT_PASSWORD": "str", - "MQTT_PORT": "int", - "TIME_INTERVAL": "int", - "MQTT_DISCOVERY": "bool", - "MQTT_DISCOVERY_PREFIX": "str", - - "USER1_GT": "int", - "USER1_SEX": "str", - "USER1_NAME": "str", - "USER1_HEIGHT": "int", - "USER1_DOB": "str", - - "USER2_LT": "int?", - "USER2_SEX": "str?", - "USER2_NAME": "str?", - "USER2_HEIGHT": "int?", - "USER2_DOB": "str?", - - "USER3_SEX": "str?", - "USER3_NAME": "str?", - "USER3_HEIGHT": "int?", - "USER3_DOB": "str?" - } +{ + "name": "Xiaomi Mi Scale", + "version": "0.1.6", + "slug": "xiaomi_mi_scale", + "description": "Read weight measurements from Xiamomi scale via BLE", + "url": "https://github.com/lolouk44/xiaomi_mi_scale_ha_add_on", + "image": "lolouk44/xiaomi-mi-scale", + "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], + "startup": "before", + "boot": "auto", + "panel_admin": false, + "host_network": true, + "privileged": ["NET_ADMIN", "SYS_ADMIN"], + + "options": { + "HCI_DEV": "hci0", + "MISCALE_MAC": "00:00:00:00:00:00", + "MQTT_PREFIX": "miScale", + "MQTT_HOST": "192.168.0.1", + "MQTT_USERNAME": "user", + "MQTT_PASSWORD": "passwd", + "MQTT_PORT": 1883, + "TIME_INTERVAL": 30, + "MQTT_DISCOVERY": true, + "MQTT_DISCOVERY_PREFIX": "homeassistant", + + "USER1_GT": 70, + "USER1_SEX": "male", + "USER1_NAME": "Jo", + "USER1_HEIGHT": 175, + "USER1_DOB": "1990-01-01", + + "USER2_LT": 35, + "USER2_SEX": "female", + "USER2_NAME": "Serena", + "USER2_HEIGHT": 95, + "USER2_DOB": "1990-01-01", + + "USER3_SEX": "female", + "USER3_NAME": "Missy", + "USER3_HEIGHT": 150, + "USER3_DOB": "1990-01-01" + + }, + "schema": { + "HCI_DEV": "str?", + "MISCALE_MAC": "str", + "MQTT_PREFIX": "str?", + "MQTT_HOST": "str", + "MQTT_USERNAME": "str?", + "MQTT_PASSWORD": "str?", + "MQTT_PORT": "int?", + "TIME_INTERVAL": "int?", + "MQTT_DISCOVERY": "bool?", + "MQTT_DISCOVERY_PREFIX": "str?", + + "USER1_GT": "int", + "USER1_SEX": "str", + "USER1_NAME": "str", + "USER1_HEIGHT": "int", + "USER1_DOB": "str", + + "USER2_LT": "int?", + "USER2_SEX": "str?", + "USER2_NAME": "str?", + "USER2_HEIGHT": "int?", + "USER2_DOB": "str?", + + "USER3_SEX": "str?", + "USER3_NAME": "str?", + "USER3_HEIGHT": "int?", + "USER3_DOB": "str?" + } } \ No newline at end of file diff --git a/dockerscripts/cmd.sh b/mi-scale/dockerscripts/cmd.sh similarity index 100% rename from dockerscripts/cmd.sh rename to mi-scale/dockerscripts/cmd.sh diff --git a/dockerscripts/entrypoint.sh b/mi-scale/dockerscripts/entrypoint.sh similarity index 100% rename from dockerscripts/entrypoint.sh rename to mi-scale/dockerscripts/entrypoint.sh diff --git a/icon.png b/mi-scale/icon.png similarity index 100% rename from icon.png rename to mi-scale/icon.png diff --git a/logo.png b/mi-scale/logo.png similarity index 100% rename from logo.png rename to mi-scale/logo.png diff --git a/src/Xiaomi_Scale.py b/mi-scale/src/Xiaomi_Scale.py similarity index 74% rename from src/Xiaomi_Scale.py rename to mi-scale/src/Xiaomi_Scale.py index a0a7497..7cc026f 100644 --- a/src/Xiaomi_Scale.py +++ b/mi-scale/src/Xiaomi_Scale.py @@ -1,242 +1,334 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -from __future__ import print_function -import argparse -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.publish as publish -from datetime import datetime -import json - -import Xiaomi_Scale_Body_Metrics - - - -# First Log msg -sys.stdout.write(' \n') -sys.stdout.write('-------------------------------------\n') -sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Starting Xiaomi mi Scale...\n") - -# Configuraiton... -# Trying To Load Config From options.json (HA Add-On) -try: - with open('/data/options.json') as json_file: - sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Loading Config From Add-On Options...\n") - data = json.load(json_file) - MISCALE_MAC = data["MISCALE_MAC"] - MQTT_USERNAME = None if(data["MQTT_USERNAME"] == "") else data["MQTT_USERNAME"] - MQTT_PASSWORD = None if(data["MQTT_PASSWORD"] == "") else data["MQTT_PASSWORD"] - MQTT_HOST = data["MQTT_HOST"] - MQTT_PORT = int(data["MQTT_PORT"]) - MQTT_PREFIX = data["MQTT_PREFIX"] - TIME_INTERVAL = int(data["TIME_INTERVAL"]) - MQTT_DISCOVERY = data["MQTT_DISCOVERY"] - MQTT_DISCOVERY_PREFIX = data["MQTT_DISCOVERY_PREFIX"] - HCI_DEV = data["HCI_DEV"][-1] - - # User Variables... - USER1_GT = int(data["USER1_GT"]) - USER1_SEX = data["USER1_SEX"] - USER1_NAME = data["USER1_NAME"] - USER1_HEIGHT = int(data["USER1_HEIGHT"]) - USER1_DOB = data["USER1_DOB"] - - USER2_LT = int(data["USER2_LT"]) - USER2_SEX = data["USER2_SEX"] - USER2_NAME = data["USER2_NAME"] - USER2_HEIGHT = int(data["USER2_HEIGHT"]) - USER2_DOB = data["USER2_DOB"] - - USER3_SEX = data["USER3_SEX"] - USER3_NAME = data["USER3_NAME"] - USER3_HEIGHT = int(data["USER3_HEIGHT"]) - USER3_DOB = data["USER3_DOB"] - sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Config Loaded...\n") - -# Failed to open options.json, Loading Config From Environment (Not HA Add-On) -except FileNotFoundError: - pass - sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Loading Config From OS Environment...\n") - MISCALE_MAC = os.getenv('MISCALE_MAC', '') - MQTT_USERNAME = os.getenv('MQTT_USERNAME', 'username') - MQTT_PASSWORD = os.getenv('MQTT_PASSWORD', None) - MQTT_HOST = os.getenv('MQTT_HOST', '127.0.0.1') - MQTT_PORT = int(os.getenv('MQTT_PORT', 1883)) - MQTT_PREFIX = os.getenv('MQTT_PREFIX', 'miscale') - TIME_INTERVAL = int(os.getenv('TIME_INTERVAL', 30)) - MQTT_DISCOVERY = os.getenv('MQTT_DISCOVERY',True) - MQTT_DISCOVERY_PREFIX = os.getenv('MQTT_DISCOVERY_PREFIX','homeassistant') - HCI_DEV = os.getenv('HCI_DEV', 'hci0')[-1] - - # 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 - USER1_SEX = os.getenv('USER1_SEX', 'male') - USER1_NAME = os.getenv('USER1_NAME', 'David') # Name of the user - USER1_HEIGHT = int(os.getenv('USER1_HEIGHT', '175')) # Height (in cm) of the user - USER1_DOB = os.getenv('USER1_DOB', '1988-09-30') # DOB (in yyyy-mm-dd format) - - USER2_LT = int(os.getenv('USER2_LT', '55')) # If the weight is less than this number, we'll assume that we're weighing User #2 - USER2_SEX = os.getenv('USER2_SEX', 'female') - USER2_NAME = os.getenv('USER2_NAME', 'Joanne') # Name of the user - USER2_HEIGHT = int(os.getenv('USER2_HEIGHT', '155')) # Height (in cm) of the user - USER2_DOB = os.getenv('USER2_DOB', '1988-10-20') # DOB (in yyyy-mm-dd format) - - USER3_SEX = os.getenv('USER3_SEX', 'male') - USER3_NAME = os.getenv('USER3_NAME', 'Unknown User') # Name of the user - USER3_HEIGHT = int(os.getenv('USER3_HEIGHT', '175')) # Height (in cm) of the user - USER3_DOB = os.getenv('USER3_DOB', '1988-01-01') # DOB (in yyyy-mm-dd format) - sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Config Loaded...\n") - -OLD_MEASURE = '' - -def discovery(): - for MQTTUser in (USER1_NAME,USER2_NAME,USER3_NAME): - message = '{"name": "' + MQTTUser + ' Weight",' - message+= '"state_topic": "miScale/' + MQTTUser + '/weight","value_template": "{{ value_json.Weight }}","unit_of_measurement": "kg",' - message+= '"json_attributes_topic": "miScale/' + MQTTUser + '/weight","icon": "mdi:scale-bathroom"}' - publish.single( - MQTT_DISCOVERY_PREFIX + '/sensor/' + MQTT_PREFIX + '/' + MQTTUser + '/config', - message, - retain=True, - hostname=MQTT_HOST, - port=MQTT_PORT, - auth={'username':MQTT_USERNAME, 'password':MQTT_PASSWORD} - ) - sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Discovery Completed...\n") - - -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 __init__(self): - DefaultDelegate.__init__(self) - - 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): - 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) - - 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): - 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) - - - def _publish(self, weight, unit, mitdatetime, hasImpedance, miimpedance): - 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()) + '"' - - 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: - sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Publishing data to topic {MQTT_PREFIX + '/' + user + '/weight'}: {message}\n") - publish.single( - MQTT_PREFIX + '/' + user + '/weight', - message, - # qos=1, #Removed qos=1 as incorrect connection details will result in the client waiting for ack from broker - retain=True, - hostname=MQTT_HOST, - port=MQTT_PORT, - auth={'username':MQTT_USERNAME, 'password':MQTT_PASSWORD} - ) - sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Data Published ...\n") - except Exception as error: - sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Could not publish to MQTT: {error}\n") - raise - -def main(): - if MQTT_DISCOVERY: - discovery() - BluetoothFailCounter = 0 - while True: - try: - scanner = btle.Scanner(HCI_DEV).withDelegate(ScanProcessor()) - scanner.scan(5) # Adding passive=True to try and fix issues on RPi devices - except BTLEDisconnectError as error: - sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - btle disconnected: {error}\n") - pass - except BTLEManagementError as error: - sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Bluetooth connection error: {error}\n") - if BluetoothFailCounter >= 4: - sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - 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 Exception as error: - sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Error while running the script: {error}\n") - pass - else: - BluetoothFailCounter = 0 - time.sleep(TIME_INTERVAL) - -if __name__ == "__main__": - main() +#!/usr/bin/python +# -*- coding: utf-8 -*- +from __future__ import print_function +import argparse +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.publish as publish +from datetime import datetime +import json + +import Xiaomi_Scale_Body_Metrics + + + +# First Log msg +sys.stdout.write(' \n') +sys.stdout.write('-------------------------------------\n') +sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Starting Xiaomi mi Scale...\n") + +# Configuraiton... +# Trying To Load Config From options.json (HA Add-On) +try: + with open('/data/options.json') as json_file: + sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Loading Config From Add-On Options...\n") + data = json.load(json_file) + try: + MISCALE_MAC = data["MISCALE_MAC"] + except: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - MAC Address not provided...\n") + raise + try: + MQTT_USERNAME = data["MQTT_USERNAME"] + except: + MQTT_USERNAME = None + pass + try: + MQTT_PASSWORD = data["MQTT_PASSWORD"] + except: + MQTT_PASSWORD = None + pass + try: + MQTT_HOST = data["MQTT_HOST"] + except: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - MQTT Host not provided...\n") + raise + try: + MQTT_PORT = int(data["MQTT_PORT"]) + except: + MQTT_PORT = 1883 + pass + try: + MQTT_PREFIX = data["MQTT_PREFIX"] + except: + MQTT_PREFIX = "miScale" + pass + try: + TIME_INTERVAL = int(data["TIME_INTERVAL"]) + except: + TIME_INTERVAL = 30 + pass + try: + MQTT_DISCOVERY = data["MQTT_DISCOVERY"] + except: + MQTT_DISCOVERY = True + pass + try: + MQTT_DISCOVERY_PREFIX = data["MQTT_DISCOVERY_PREFIX"] + except: + MQTT_DISCOVERY_PREFIX = "homeassistant" + pass + try: + HCI_DEV = data["HCI_DEV"][-1] + except: + HCI_DEV = "hci0"[-1] + pass + try: + USER1_GT = int(data["USER1_GT"]) + except: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - USER1_GT not provided...\n") + raise + try: + USER1_SEX = data["USER1_SEX"] + except: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - USER1_SEX not provided...\n") + raise + try: + USER1_NAME = data["USER1_NAME"] + except: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - USER1_NAME not provided...\n") + raise + try: + USER1_HEIGHT = int(data["USER1_HEIGHT"]) + except: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - USER1_HEIGHT not provided...\n") + raise + try: + USER1_DOB = data["USER1_DOB"] + except: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - USER1_DOB not provided...\n") + raise + try: + USER2_LT = int(data["USER2_LT"]) + except: + USER2_LT = USER1_GT + pass + try: + USER2_SEX = data["USER2_SEX"] + except: + USER2_SEX = "female" + pass + try: + USER2_NAME = data["USER2_NAME"] + except: + USER2_NAME = "Serena" + pass + try: + USER2_HEIGHT = int(data["USER2_HEIGHT"]) + except: + USER2_HEIGHT = 95 + pass + try: + USER2_DOB = data["USER2_DOB"] + except: + USER2_DOB = "1990-01-01" + pass + try: + USER3_SEX = data["USER3_SEX"] + except: + USER3_SEX = "female" + pass + try: + USER3_NAME = data["USER3_NAME"] + except: + USER3_NAME = "Missy" + pass + try: + USER3_HEIGHT = int(data["USER3_HEIGHT"]) + except: + USER3_HEIGHT = 150 + pass + try: + USER3_DOB = data["USER3_DOB"] + except: + USER3_DOB = "1990-01-01" + pass + sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Config Loaded...\n") + +# Failed to open options.json, Loading Config From Environment (Not HA Add-On) +except FileNotFoundError: + pass + sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Loading Config From OS Environment...\n") + MISCALE_MAC = os.getenv('MISCALE_MAC', '') + MQTT_USERNAME = os.getenv('MQTT_USERNAME', 'username') + MQTT_PASSWORD = os.getenv('MQTT_PASSWORD', None) + MQTT_HOST = os.getenv('MQTT_HOST', '127.0.0.1') + MQTT_PORT = int(os.getenv('MQTT_PORT', 1883)) + MQTT_PREFIX = os.getenv('MQTT_PREFIX', 'miscale') + TIME_INTERVAL = int(os.getenv('TIME_INTERVAL', 30)) + MQTT_DISCOVERY = os.getenv('MQTT_DISCOVERY',True) + MQTT_DISCOVERY_PREFIX = os.getenv('MQTT_DISCOVERY_PREFIX','homeassistant') + HCI_DEV = os.getenv('HCI_DEV', 'hci0')[-1] + + # 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 + USER1_SEX = os.getenv('USER1_SEX', 'male') + USER1_NAME = os.getenv('USER1_NAME', 'David') # Name of the user + USER1_HEIGHT = int(os.getenv('USER1_HEIGHT', '175')) # Height (in cm) of the user + USER1_DOB = os.getenv('USER1_DOB', '1988-09-30') # DOB (in yyyy-mm-dd format) + + USER2_LT = int(os.getenv('USER2_LT', '55')) # If the weight is less than this number, we'll assume that we're weighing User #2 + USER2_SEX = os.getenv('USER2_SEX', 'female') + USER2_NAME = os.getenv('USER2_NAME', 'Joanne') # Name of the user + USER2_HEIGHT = int(os.getenv('USER2_HEIGHT', '155')) # Height (in cm) of the user + USER2_DOB = os.getenv('USER2_DOB', '1988-10-20') # DOB (in yyyy-mm-dd format) + + USER3_SEX = os.getenv('USER3_SEX', 'male') + USER3_NAME = os.getenv('USER3_NAME', 'Unknown User') # Name of the user + USER3_HEIGHT = int(os.getenv('USER3_HEIGHT', '175')) # Height (in cm) of the user + USER3_DOB = os.getenv('USER3_DOB', '1988-01-01') # DOB (in yyyy-mm-dd format) + sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Config Loaded...\n") + +OLD_MEASURE = '' + +def discovery(): + for MQTTUser in (USER1_NAME,USER2_NAME,USER3_NAME): + message = '{"name": "' + MQTTUser + ' Weight",' + message+= '"state_topic": "miScale/' + MQTTUser + '/weight","value_template": "{{ value_json.Weight }}","unit_of_measurement": "kg",' + message+= '"json_attributes_topic": "miScale/' + MQTTUser + '/weight","icon": "mdi:scale-bathroom"}' + publish.single( + MQTT_DISCOVERY_PREFIX + '/sensor/' + MQTT_PREFIX + '/' + MQTTUser + '/config', + message, + retain=True, + hostname=MQTT_HOST, + port=MQTT_PORT, + auth={'username':MQTT_USERNAME, 'password':MQTT_PASSWORD} + ) + sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Discovery Completed...\n") + + +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 __init__(self): + DefaultDelegate.__init__(self) + + 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): + 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) + + 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): + 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) + + + def _publish(self, weight, unit, mitdatetime, hasImpedance, miimpedance): + 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()) + '"' + + 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: + sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Publishing data to topic {MQTT_PREFIX + '/' + user + '/weight'}: {message}\n") + publish.single( + MQTT_PREFIX + '/' + user + '/weight', + message, + # qos=1, #Removed qos=1 as incorrect connection details will result in the client waiting for ack from broker + retain=True, + hostname=MQTT_HOST, + port=MQTT_PORT, + auth={'username':MQTT_USERNAME, 'password':MQTT_PASSWORD} + ) + sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Data Published ...\n") + except Exception as error: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Could not publish to MQTT: {error}\n") + raise + +def main(): + if MQTT_DISCOVERY: + discovery() + BluetoothFailCounter = 0 + while True: + try: + scanner = btle.Scanner(HCI_DEV).withDelegate(ScanProcessor()) + scanner.scan(5) # Adding passive=True to try and fix issues on RPi devices + except BTLEDisconnectError as error: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - btle disconnected: {error}\n") + pass + except BTLEManagementError as error: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Bluetooth connection error: {error}\n") + if BluetoothFailCounter >= 4: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - 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 Exception as error: + sys.stderr.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Error while running the script: {error}\n") + pass + else: + BluetoothFailCounter = 0 + time.sleep(TIME_INTERVAL) + +if __name__ == "__main__": + main() diff --git a/src/Xiaomi_Scale_Body_Metrics.py b/mi-scale/src/Xiaomi_Scale_Body_Metrics.py similarity index 100% rename from src/Xiaomi_Scale_Body_Metrics.py rename to mi-scale/src/Xiaomi_Scale_Body_Metrics.py diff --git a/src/body_scales.py b/mi-scale/src/body_scales.py similarity index 97% rename from src/body_scales.py rename to mi-scale/src/body_scales.py index 2f265c2..33a7c16 100644 --- a/src/body_scales.py +++ b/mi-scale/src/body_scales.py @@ -1,155 +1,155 @@ -class bodyScales: - def __init__(self, age, height, sex, weight, scaleType='xiaomi'): - self.age = age - self.height = height - self.sex = sex - self.weight = weight - - if scaleType == 'xiaomi': - self.scaleType = 'xiaomi' - else: - self.scaleType = 'holtek' - - # Get BMI scale - def getBMIScale(self): - if self.scaleType == 'xiaomi': - # Amazfit/new mi fit - #return [18.5, 24, 28] - # Old mi fit // amazfit for body figure - return [18.5, 25.0, 28.0, 32.0] - elif self.scaleType == 'holtek': - return [18.5, 25.0, 30.0] - - # Get fat percentage scale - def getFatPercentageScale(self): - # The included tables where quite strange, maybe bogus, replaced them with better ones... - if self.scaleType == 'xiaomi': - scales = [ - {'min': 0, 'max': 12, 'female': [12.0, 21.0, 30.0, 34.0], 'male': [7.0, 16.0, 25.0, 30.0]}, - {'min': 12, 'max': 14, 'female': [15.0, 24.0, 33.0, 37.0], 'male': [7.0, 16.0, 25.0, 30.0]}, - {'min': 14, 'max': 16, 'female': [18.0, 27.0, 36.0, 40.0], 'male': [7.0, 16.0, 25.0, 30.0]}, - {'min': 16, 'max': 18, 'female': [20.0, 28.0, 37.0, 41.0], 'male': [7.0, 16.0, 25.0, 30.0]}, - {'min': 18, 'max': 40, 'female': [21.0, 28.0, 35.0, 40.0], 'male': [11.0, 17.0, 22.0, 27.0]}, - {'min': 40, 'max': 60, 'female': [22.0, 29.0, 36.0, 41.0], 'male': [12.0, 18.0, 23.0, 28.0]}, - {'min': 60, 'max': 100, 'female': [23.0, 30.0, 37.0, 42.0], 'male': [14.0, 20.0, 25.0, 30.0]}, - ] - - elif self.scaleType == 'holtek': - scales = [ - {'min': 0, 'max': 21, 'female': [18, 23, 30, 35], 'male': [8, 14, 21, 25]}, - {'min': 21, 'max': 26, 'female': [19, 24, 30, 35], 'male': [10, 15, 22, 26]}, - {'min': 26, 'max': 31, 'female': [20, 25, 31, 36], 'male': [11, 16, 21, 27]}, - {'min': 31, 'max': 36, 'female': [21, 26, 33, 36], 'male': [13, 17, 25, 28]}, - {'min': 36, 'max': 41, 'female': [22, 27, 34, 37], 'male': [15, 20, 26, 29]}, - {'min': 41, 'max': 46, 'female': [23, 28, 35, 38], 'male': [16, 22, 27, 30]}, - {'min': 46, 'max': 51, 'female': [24, 30, 36, 38], 'male': [17, 23, 29, 31]}, - {'min': 51, 'max': 56, 'female': [26, 31, 36, 39], 'male': [19, 25, 30, 33]}, - {'min': 56, 'max': 100, 'female': [27, 32, 37, 40], 'male': [21, 26, 31, 34]}, - ] - - for scale in scales: - if self.age >= scale['min'] and self.age < scale['max']: - return scale[self.sex] - - # Get muscle mass scale - def getMuscleMassScale(self): - if self.scaleType == 'xiaomi': - scales = [ - {'min': {'male': 170, 'female': 160}, 'female': [36.5, 42.6], 'male': [49.4, 59.5]}, - {'min': {'male': 160, 'female': 150}, 'female': [32.9, 37.6], 'male': [44.0, 52.5]}, - {'min': {'male': 0, 'female': 0}, 'female': [29.1, 34.8], 'male': [38.5, 46.6]}, - ] - elif self.scaleType == 'holtek': - scales = [ - {'min': {'male': 170, 'female': 170}, 'female': [36.5, 42.5], 'male': [49.5, 59.4]}, - {'min': {'male': 160, 'female': 160}, 'female': [32.9, 37.5], 'male': [44.0, 52.4]}, - {'min': {'male': 0, 'female': 0}, 'female': [29.1, 34.7], 'male': [38.5, 46.5]} - ] - - for scale in scales: - if self.height >= scale['min'][self.sex]: - return scale[self.sex] - - - - # Get water percentage scale - def getWaterPercentageScale(self): - if self.scaleType == 'xiaomi': - if self.sex == 'male': - return [55.0, 65.1] - elif self.sex == 'female': - return [45.0, 60.1] - elif self.scaleType == 'holtek': - return [53, 67] - - - # Get visceral fat scale - def getVisceralFatScale(self): - # Actually the same in mi fit/amazfit and holtek's sdk - return [10.0, 15.0] - - - # Get bone mass scale - def getBoneMassScale(self): - if self.scaleType == 'xiaomi': - scales = [ - {'male': {'min': 75.0, 'scale': [2.0, 4.2]}, 'female': {'min': 60.0, 'scale': [1.8, 3.9]}}, - {'male': {'min': 60.0, 'scale': [1.9, 4.1]}, 'female': {'min': 45.0, 'scale': [1.5, 3.8]}}, - {'male': {'min': 0.0, 'scale': [1.6, 3.9]}, 'female': {'min': 0.0, 'scale': [1.3, 3.6]}}, - ] - - for scale in scales: - if self.weight >= scale[self.sex]['min']: - return scale[self.sex]['scale'] - - elif self.scaleType == 'holtek': - scales = [ - {'female': {'min': 60, 'optimal': 2.5}, 'male': {'min': 75, 'optimal': 3.2}}, - {'female': {'min': 45, 'optimal': 2.2}, 'male': {'min': 69, 'optimal': 2.9}}, - {'female': {'min': 0, 'optimal': 1.8}, 'male': {'min': 0, 'optimal': 2.5}} - ] - - for scale in scales: - if self.weight >= scale[self.sex]['min']: - return [scale[self.sex]['optimal']-1, scale[self.sex]['optimal']+1] - - - # Get BMR scale - def getBMRScale(self): - if self.scaleType == 'xiaomi': - coefficients = { - 'male': {30: 21.6, 50: 20.07, 100: 19.35}, - 'female': {30: 21.24, 50: 19.53, 100: 18.63} - } - elif self.scaleType == 'holtek': - coefficients = { - 'female': {12: 34, 15: 29, 17: 24, 29: 22, 50: 20, 120: 19}, - 'male': {12: 36, 15: 30, 17: 26, 29: 23, 50: 21, 120: 20} - } - - for age, coefficient in coefficients[self.sex].items(): - if self.age < age: - return [self.weight * coefficient] - - - # Get protein scale (hardcoded in mi fit) - def getProteinPercentageScale(self): - # Actually the same in mi fit and holtek's sdk - return [16, 20] - - # Get ideal weight scale (BMI scale converted to weights) - def getIdealWeightScale(self): - scale = [] - for bmiScale in self.getBMIScale(): - scale.append((bmiScale*self.height)*self.height/10000) - return scale - - # Get Body Score scale - def getBodyScoreScale(self): - # very bad, bad, normal, good, better - return [50.0, 60.0, 80.0, 90.0] - - # Return body type scale - def getBodyTypeScale(self): - return ['obese', 'overweight', 'thick-set', 'lack-exerscise', 'balanced', 'balanced-muscular', 'skinny', 'balanced-skinny', 'skinny-muscular'] - +class bodyScales: + def __init__(self, age, height, sex, weight, scaleType='xiaomi'): + self.age = age + self.height = height + self.sex = sex + self.weight = weight + + if scaleType == 'xiaomi': + self.scaleType = 'xiaomi' + else: + self.scaleType = 'holtek' + + # Get BMI scale + def getBMIScale(self): + if self.scaleType == 'xiaomi': + # Amazfit/new mi fit + #return [18.5, 24, 28] + # Old mi fit // amazfit for body figure + return [18.5, 25.0, 28.0, 32.0] + elif self.scaleType == 'holtek': + return [18.5, 25.0, 30.0] + + # Get fat percentage scale + def getFatPercentageScale(self): + # The included tables where quite strange, maybe bogus, replaced them with better ones... + if self.scaleType == 'xiaomi': + scales = [ + {'min': 0, 'max': 12, 'female': [12.0, 21.0, 30.0, 34.0], 'male': [7.0, 16.0, 25.0, 30.0]}, + {'min': 12, 'max': 14, 'female': [15.0, 24.0, 33.0, 37.0], 'male': [7.0, 16.0, 25.0, 30.0]}, + {'min': 14, 'max': 16, 'female': [18.0, 27.0, 36.0, 40.0], 'male': [7.0, 16.0, 25.0, 30.0]}, + {'min': 16, 'max': 18, 'female': [20.0, 28.0, 37.0, 41.0], 'male': [7.0, 16.0, 25.0, 30.0]}, + {'min': 18, 'max': 40, 'female': [21.0, 28.0, 35.0, 40.0], 'male': [11.0, 17.0, 22.0, 27.0]}, + {'min': 40, 'max': 60, 'female': [22.0, 29.0, 36.0, 41.0], 'male': [12.0, 18.0, 23.0, 28.0]}, + {'min': 60, 'max': 100, 'female': [23.0, 30.0, 37.0, 42.0], 'male': [14.0, 20.0, 25.0, 30.0]}, + ] + + elif self.scaleType == 'holtek': + scales = [ + {'min': 0, 'max': 21, 'female': [18, 23, 30, 35], 'male': [8, 14, 21, 25]}, + {'min': 21, 'max': 26, 'female': [19, 24, 30, 35], 'male': [10, 15, 22, 26]}, + {'min': 26, 'max': 31, 'female': [20, 25, 31, 36], 'male': [11, 16, 21, 27]}, + {'min': 31, 'max': 36, 'female': [21, 26, 33, 36], 'male': [13, 17, 25, 28]}, + {'min': 36, 'max': 41, 'female': [22, 27, 34, 37], 'male': [15, 20, 26, 29]}, + {'min': 41, 'max': 46, 'female': [23, 28, 35, 38], 'male': [16, 22, 27, 30]}, + {'min': 46, 'max': 51, 'female': [24, 30, 36, 38], 'male': [17, 23, 29, 31]}, + {'min': 51, 'max': 56, 'female': [26, 31, 36, 39], 'male': [19, 25, 30, 33]}, + {'min': 56, 'max': 100, 'female': [27, 32, 37, 40], 'male': [21, 26, 31, 34]}, + ] + + for scale in scales: + if self.age >= scale['min'] and self.age < scale['max']: + return scale[self.sex] + + # Get muscle mass scale + def getMuscleMassScale(self): + if self.scaleType == 'xiaomi': + scales = [ + {'min': {'male': 170, 'female': 160}, 'female': [36.5, 42.6], 'male': [49.4, 59.5]}, + {'min': {'male': 160, 'female': 150}, 'female': [32.9, 37.6], 'male': [44.0, 52.5]}, + {'min': {'male': 0, 'female': 0}, 'female': [29.1, 34.8], 'male': [38.5, 46.6]}, + ] + elif self.scaleType == 'holtek': + scales = [ + {'min': {'male': 170, 'female': 170}, 'female': [36.5, 42.5], 'male': [49.5, 59.4]}, + {'min': {'male': 160, 'female': 160}, 'female': [32.9, 37.5], 'male': [44.0, 52.4]}, + {'min': {'male': 0, 'female': 0}, 'female': [29.1, 34.7], 'male': [38.5, 46.5]} + ] + + for scale in scales: + if self.height >= scale['min'][self.sex]: + return scale[self.sex] + + + + # Get water percentage scale + def getWaterPercentageScale(self): + if self.scaleType == 'xiaomi': + if self.sex == 'male': + return [55.0, 65.1] + elif self.sex == 'female': + return [45.0, 60.1] + elif self.scaleType == 'holtek': + return [53, 67] + + + # Get visceral fat scale + def getVisceralFatScale(self): + # Actually the same in mi fit/amazfit and holtek's sdk + return [10.0, 15.0] + + + # Get bone mass scale + def getBoneMassScale(self): + if self.scaleType == 'xiaomi': + scales = [ + {'male': {'min': 75.0, 'scale': [2.0, 4.2]}, 'female': {'min': 60.0, 'scale': [1.8, 3.9]}}, + {'male': {'min': 60.0, 'scale': [1.9, 4.1]}, 'female': {'min': 45.0, 'scale': [1.5, 3.8]}}, + {'male': {'min': 0.0, 'scale': [1.6, 3.9]}, 'female': {'min': 0.0, 'scale': [1.3, 3.6]}}, + ] + + for scale in scales: + if self.weight >= scale[self.sex]['min']: + return scale[self.sex]['scale'] + + elif self.scaleType == 'holtek': + scales = [ + {'female': {'min': 60, 'optimal': 2.5}, 'male': {'min': 75, 'optimal': 3.2}}, + {'female': {'min': 45, 'optimal': 2.2}, 'male': {'min': 69, 'optimal': 2.9}}, + {'female': {'min': 0, 'optimal': 1.8}, 'male': {'min': 0, 'optimal': 2.5}} + ] + + for scale in scales: + if self.weight >= scale[self.sex]['min']: + return [scale[self.sex]['optimal']-1, scale[self.sex]['optimal']+1] + + + # Get BMR scale + def getBMRScale(self): + if self.scaleType == 'xiaomi': + coefficients = { + 'male': {30: 21.6, 50: 20.07, 100: 19.35}, + 'female': {30: 21.24, 50: 19.53, 100: 18.63} + } + elif self.scaleType == 'holtek': + coefficients = { + 'female': {12: 34, 15: 29, 17: 24, 29: 22, 50: 20, 120: 19}, + 'male': {12: 36, 15: 30, 17: 26, 29: 23, 50: 21, 120: 20} + } + + for age, coefficient in coefficients[self.sex].items(): + if self.age < age: + return [self.weight * coefficient] + + + # Get protein scale (hardcoded in mi fit) + def getProteinPercentageScale(self): + # Actually the same in mi fit and holtek's sdk + return [16, 20] + + # Get ideal weight scale (BMI scale converted to weights) + def getIdealWeightScale(self): + scale = [] + for bmiScale in self.getBMIScale(): + scale.append((bmiScale*self.height)*self.height/10000) + return scale + + # Get Body Score scale + def getBodyScoreScale(self): + # very bad, bad, normal, good, better + return [50.0, 60.0, 80.0, 90.0] + + # Return body type scale + def getBodyTypeScale(self): + return ['obese', 'overweight', 'thick-set', 'lack-exerscise', 'balanced', 'balanced-muscular', 'skinny', 'balanced-skinny', 'skinny-muscular'] + diff --git a/src/body_score.py b/mi-scale/src/body_score.py similarity index 100% rename from src/body_score.py rename to mi-scale/src/body_score.py diff --git a/src/requirements.txt b/mi-scale/src/requirements.txt similarity index 100% rename from src/requirements.txt rename to mi-scale/src/requirements.txt diff --git a/src/wrapper.sh b/mi-scale/src/wrapper.sh similarity index 97% rename from src/wrapper.sh rename to mi-scale/src/wrapper.sh index ba61e74..068d3ee 100644 --- a/src/wrapper.sh +++ b/mi-scale/src/wrapper.sh @@ -1,24 +1,24 @@ -#!/bin/bash - -export MISCALE_MAC=00:00:00:00:00:00 # Mac address of your scale -export MQTT_PREFIX=miScale - -export USER1_GT=70 # If the weight is greater than this number, we'll assume that we're weighing User #1 -export USER1_SEX=male -export USER1_NAME=Jo # Name of the user -export USER1_HEIGHT=175 # Height (in cm) of the user -export USER1_DOB=1990-01-01 # DOB (in yyyy-mm-dd format) - -export USER2_LT=35 # If the weight is less than this number, we'll assume that we're weighing User #2 -export USER2_SEX=female -export USER2_NAME=Sarah # Name of the user -export USER2_HEIGHT=95 # Height (in cm) of the user -export USER2_DOB=1990-01-01 # DOB (in yyyy-mm-dd format) - -export USER3_SEX=female -export USER3_NAME=Missy # Name of the user -export USER3_HEIGHT=150 # Height (in cm) of the user -export USER3_DOB=1990-01-01 # DOB (in yyyy-mm-dd format) - -MY_PWD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +#!/bin/bash + +export MISCALE_MAC=00:00:00:00:00:00 # Mac address of your scale +export MQTT_PREFIX=miScale + +export USER1_GT=70 # If the weight is greater than this number, we'll assume that we're weighing User #1 +export USER1_SEX=male +export USER1_NAME=Jo # Name of the user +export USER1_HEIGHT=175 # Height (in cm) of the user +export USER1_DOB=1990-01-01 # DOB (in yyyy-mm-dd format) + +export USER2_LT=35 # If the weight is less than this number, we'll assume that we're weighing User #2 +export USER2_SEX=female +export USER2_NAME=Sarah # Name of the user +export USER2_HEIGHT=95 # Height (in cm) of the user +export USER2_DOB=1990-01-01 # DOB (in yyyy-mm-dd format) + +export USER3_SEX=female +export USER3_NAME=Missy # Name of the user +export USER3_HEIGHT=150 # Height (in cm) of the user +export USER3_DOB=1990-01-01 # DOB (in yyyy-mm-dd format) + +MY_PWD="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" python3 $MY_PWD/Xiaomi_Scale.py \ No newline at end of file diff --git a/repository.json b/repository.json new file mode 100644 index 0000000..2d816e2 --- /dev/null +++ b/repository.json @@ -0,0 +1,5 @@ +{ + "name": "Lolouk44 Add-Ons", + "url": "https://github.com/lolouk44/hassio-addons", + "maintainer": "lolouk44" +} \ No newline at end of file