initial commit
This commit is contained in:
commit
b700f506cf
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behaviour, including error message if any.
|
||||||
|
|
||||||
|
**Expected behaviour**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Scale (please complete the following information):**
|
||||||
|
- Name
|
||||||
|
- Model #
|
||||||
|
|
||||||
|
**Device running Home Assistant (please complete the following information):**
|
||||||
|
- Device used to run the Script/Container [e.g. Raspberry Pi, NUC]
|
||||||
|
- Bluetooth device used [e.g. Built-in, USB Dongle]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
3
CHANGELOG.md
Normal file
3
CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
## [0.1.4] - 2020-06-29
|
||||||
|
### Added
|
||||||
|
- First release (version in line with non Add-On script for ease of maintenance).
|
18
Dockerfile
Normal file
18
Dockerfile
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
FROM python:3.8-slim
|
||||||
|
|
||||||
|
WORKDIR /opt/miscale
|
||||||
|
COPY src /opt/miscale
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y \
|
||||||
|
bluez \
|
||||||
|
python-pip \
|
||||||
|
libglib2.0-dev && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
|
||||||
|
# Copy in docker scripts to root of container...
|
||||||
|
COPY dockerscripts/ /
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["/cmd.sh"]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020 lolouk44
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
97
README.md
Normal file
97
README.md
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
# 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 based on the Xiaomi Mi Fit app.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
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
|
BIN
Screenshots/addon.png
Normal file
BIN
Screenshots/addon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
BIN
Screenshots/addon_store.png
Normal file
BIN
Screenshots/addon_store.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
69
config.json
Normal file
69
config.json
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{
|
||||||
|
"name": "Xiaomi Mi Scale",
|
||||||
|
"version": "0.1.3",
|
||||||
|
"slug": "xiaomi_mi_scale",
|
||||||
|
"description": "Read weight measurements from Xiamomi scale via BLE",
|
||||||
|
"url": "https://github.com/lolouk44/xiaomi_mi_scale_ha_add_on",
|
||||||
|
"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,
|
||||||
|
|
||||||
|
"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",
|
||||||
|
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
2
dockerscripts/cmd.sh
Normal file
2
dockerscripts/cmd.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
set -e
|
||||||
|
python3 -u /opt/miscale/Xiaomi_Scale.py
|
3
dockerscripts/entrypoint.sh
Normal file
3
dockerscripts/entrypoint.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
exec "$@"
|
227
src/Xiaomi_Scale.py
Normal file
227
src/Xiaomi_Scale.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
#!/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"]
|
||||||
|
if(data["MQTT_USERNAME"] == ""):
|
||||||
|
MQTT_USERNAME = None
|
||||||
|
else:
|
||||||
|
MQTT_USERNAME = data["MQTT_USERNAME"]
|
||||||
|
if(data["MQTT_PASSWORD"] == ""):
|
||||||
|
MQTT_PASSWORD = None
|
||||||
|
else:
|
||||||
|
MQTT_PASSWORD = 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"])
|
||||||
|
HCI_DEV = data["HCI_DEV"][-1]
|
||||||
|
OLD_MEASURE = ''
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
HCI_DEV = os.getenv('HCI_DEV', 'hci0')[-1]
|
||||||
|
OLD_MEASURE = ''
|
||||||
|
|
||||||
|
# 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")
|
||||||
|
|
||||||
|
|
||||||
|
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():
|
||||||
|
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()
|
223
src/Xiaomi_Scale_Body_Metrics.py
Normal file
223
src/Xiaomi_Scale_Body_Metrics.py
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
from math import floor
|
||||||
|
import sys
|
||||||
|
from body_scales import bodyScales
|
||||||
|
|
||||||
|
class bodyMetrics:
|
||||||
|
def __init__(self, weight, height, age, sex, impedance):
|
||||||
|
self.weight = weight
|
||||||
|
self.height = height
|
||||||
|
self.age = age
|
||||||
|
self.sex = sex
|
||||||
|
self.impedance = impedance
|
||||||
|
self.scales = bodyScales(age, height, sex, weight)
|
||||||
|
|
||||||
|
# Check for potential out of boundaries
|
||||||
|
if self.height > 220:
|
||||||
|
print("Height is too high (limit: >220cm) or scale is sleeping")
|
||||||
|
sys.stderr.write('Height is over 220cm or scale is sleeping\n')
|
||||||
|
exit()
|
||||||
|
elif weight < 10 or weight > 200:
|
||||||
|
print("Weight is either too low or too high (limits: <10kg and >200kg) or scale is sleeping")
|
||||||
|
sys.stderr.write('Weight is above 10kg or below 200kg or scale is sleeping\n')
|
||||||
|
exit()
|
||||||
|
elif age > 99:
|
||||||
|
print("Age is too high (limit >99 years) or scale is sleeping")
|
||||||
|
sys.stderr.write('Age is above 99 years or scale is sleeping\n')
|
||||||
|
exit()
|
||||||
|
elif impedance > 3000:
|
||||||
|
print("Impedance is above 3000ohm or scale is sleeping")
|
||||||
|
sys.stderr.write('Impedance is above 3000ohm or scale is sleeping\n')
|
||||||
|
exit()
|
||||||
|
|
||||||
|
# Set the value to a boundary if it overflows
|
||||||
|
def checkValueOverflow(self, value, minimum, maximum):
|
||||||
|
if value < minimum:
|
||||||
|
return minimum
|
||||||
|
elif value > maximum:
|
||||||
|
return maximum
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Get LBM coefficient (with impedance)
|
||||||
|
def getLBMCoefficient(self):
|
||||||
|
lbm = (self.height * 9.058 / 100) * (self.height / 100)
|
||||||
|
lbm += self.weight * 0.32 + 12.226
|
||||||
|
lbm -= self.impedance * 0.0068
|
||||||
|
lbm -= self.age * 0.0542
|
||||||
|
return lbm
|
||||||
|
|
||||||
|
# Get BMR
|
||||||
|
def getBMR(self):
|
||||||
|
if self.sex == 'female':
|
||||||
|
bmr = 864.6 + self.weight * 10.2036
|
||||||
|
bmr -= self.height * 0.39336
|
||||||
|
bmr -= self.age * 6.204
|
||||||
|
else:
|
||||||
|
bmr = 877.8 + self.weight * 14.916
|
||||||
|
bmr -= self.height * 0.726
|
||||||
|
bmr -= self.age * 8.976
|
||||||
|
|
||||||
|
# Capping
|
||||||
|
if self.sex == 'female' and bmr > 2996:
|
||||||
|
bmr = 5000
|
||||||
|
elif self.sex == 'male' and bmr > 2322:
|
||||||
|
bmr = 5000
|
||||||
|
return self.checkValueOverflow(bmr, 500, 10000)
|
||||||
|
|
||||||
|
# Get fat percentage
|
||||||
|
def getFatPercentage(self):
|
||||||
|
# Set a constant to remove from LBM
|
||||||
|
if self.sex == 'female' and self.age <= 49:
|
||||||
|
const = 9.25
|
||||||
|
elif self.sex == 'female' and self.age > 49:
|
||||||
|
const = 7.25
|
||||||
|
else:
|
||||||
|
const = 0.8
|
||||||
|
|
||||||
|
# Calculate body fat percentage
|
||||||
|
LBM = self.getLBMCoefficient()
|
||||||
|
|
||||||
|
if self.sex == 'male' and self.weight < 61:
|
||||||
|
coefficient = 0.98
|
||||||
|
elif self.sex == 'female' and self.weight > 60:
|
||||||
|
coefficient = 0.96
|
||||||
|
if self.height > 160:
|
||||||
|
coefficient *= 1.03
|
||||||
|
elif self.sex == 'female' and self.weight < 50:
|
||||||
|
coefficient = 1.02
|
||||||
|
if self.height > 160:
|
||||||
|
coefficient *= 1.03
|
||||||
|
else:
|
||||||
|
coefficient = 1.0
|
||||||
|
fatPercentage = (1.0 - (((LBM - const) * coefficient) / self.weight)) * 100
|
||||||
|
|
||||||
|
# Capping body fat percentage
|
||||||
|
if fatPercentage > 63:
|
||||||
|
fatPercentage = 75
|
||||||
|
return self.checkValueOverflow(fatPercentage, 5, 75)
|
||||||
|
|
||||||
|
# Get water percentage
|
||||||
|
def getWaterPercentage(self):
|
||||||
|
waterPercentage = (100 - self.getFatPercentage()) * 0.7
|
||||||
|
|
||||||
|
if (waterPercentage <= 50):
|
||||||
|
coefficient = 1.02
|
||||||
|
else:
|
||||||
|
coefficient = 0.98
|
||||||
|
|
||||||
|
# Capping water percentage
|
||||||
|
if waterPercentage * coefficient >= 65:
|
||||||
|
waterPercentage = 75
|
||||||
|
return self.checkValueOverflow(waterPercentage * coefficient, 35, 75)
|
||||||
|
|
||||||
|
# Get bone mass
|
||||||
|
def getBoneMass(self):
|
||||||
|
if self.sex == 'female':
|
||||||
|
base = 0.245691014
|
||||||
|
else:
|
||||||
|
base = 0.18016894
|
||||||
|
|
||||||
|
boneMass = (base - (self.getLBMCoefficient() * 0.05158)) * -1
|
||||||
|
|
||||||
|
if boneMass > 2.2:
|
||||||
|
boneMass += 0.1
|
||||||
|
else:
|
||||||
|
boneMass -= 0.1
|
||||||
|
|
||||||
|
# Capping boneMass
|
||||||
|
if self.sex == 'female' and boneMass > 5.1:
|
||||||
|
boneMass = 8
|
||||||
|
elif self.sex == 'male' and boneMass > 5.2:
|
||||||
|
boneMass = 8
|
||||||
|
return self.checkValueOverflow(boneMass, 0.5 , 8)
|
||||||
|
|
||||||
|
# Get muscle mass
|
||||||
|
def getMuscleMass(self):
|
||||||
|
muscleMass = self.weight - ((self.getFatPercentage() * 0.01) * self.weight) - self.getBoneMass()
|
||||||
|
|
||||||
|
# Capping muscle mass
|
||||||
|
if self.sex == 'female' and muscleMass >= 84:
|
||||||
|
muscleMass = 120
|
||||||
|
elif self.sex == 'male' and muscleMass >= 93.5:
|
||||||
|
muscleMass = 120
|
||||||
|
|
||||||
|
return self.checkValueOverflow(muscleMass, 10 ,120)
|
||||||
|
|
||||||
|
# Get Visceral Fat
|
||||||
|
def getVisceralFat(self):
|
||||||
|
if self.sex == 'female':
|
||||||
|
if self.weight > (13 - (self.height * 0.5)) * -1:
|
||||||
|
subsubcalc = ((self.height * 1.45) + (self.height * 0.1158) * self.height) - 120
|
||||||
|
subcalc = self.weight * 500 / subsubcalc
|
||||||
|
vfal = (subcalc - 6) + (self.age * 0.07)
|
||||||
|
else:
|
||||||
|
subcalc = 0.691 + (self.height * -0.0024) + (self.height * -0.0024)
|
||||||
|
vfal = (((self.height * 0.027) - (subcalc * self.weight)) * -1) + (self.age * 0.07) - self.age
|
||||||
|
else:
|
||||||
|
if self.height < self.weight * 1.6:
|
||||||
|
subcalc = ((self.height * 0.4) - (self.height * (self.height * 0.0826))) * -1
|
||||||
|
vfal = ((self.weight * 305) / (subcalc + 48)) - 2.9 + (self.age * 0.15)
|
||||||
|
else:
|
||||||
|
subcalc = 0.765 + self.height * -0.0015
|
||||||
|
vfal = (((self.height * 0.143) - (self.weight * subcalc)) * -1) + (self.age * 0.15) - 5.0
|
||||||
|
|
||||||
|
return self.checkValueOverflow(vfal, 1 ,50)
|
||||||
|
|
||||||
|
# Get BMI
|
||||||
|
def getBMI(self):
|
||||||
|
return self.checkValueOverflow(self.weight/((self.height/100)*(self.height/100)), 10, 90)
|
||||||
|
|
||||||
|
# Get ideal weight (just doing a reverse BMI, should be something better)
|
||||||
|
def getIdealWeight(self, orig=True):
|
||||||
|
# Uses mi fit algorithm (or holtek's one)
|
||||||
|
if orig and self.sex == 'female':
|
||||||
|
return (self.height - 70) * 0.6
|
||||||
|
elif orig and self.sex == 'male':
|
||||||
|
return (self.height - 80) * 0.7
|
||||||
|
else:
|
||||||
|
return self.checkValueOverflow((22*self.height)*self.height/10000, 5.5, 198)
|
||||||
|
|
||||||
|
# Get fat mass to ideal (guessing mi fit formula)
|
||||||
|
def getFatMassToIdeal(self):
|
||||||
|
mass = (self.weight * (self.getFatPercentage() / 100)) - (self.weight * (self.scales.getFatPercentageScale()[2] / 100))
|
||||||
|
if mass < 0:
|
||||||
|
return {'type': 'to_gain', 'mass': mass*-1}
|
||||||
|
else:
|
||||||
|
return {'type': 'to_lose', 'mass': mass}
|
||||||
|
|
||||||
|
# Get protetin percentage (warn: guessed formula)
|
||||||
|
def getProteinPercentage(self, orig=True):
|
||||||
|
# Use original algorithm from mi fit (or legacy guess one)
|
||||||
|
if orig:
|
||||||
|
proteinPercentage = (self.getMuscleMass() / self.weight) * 100
|
||||||
|
proteinPercentage -= self.getWaterPercentage()
|
||||||
|
else:
|
||||||
|
proteinPercentage = 100 - (floor(self.getFatPercentage() * 100) / 100)
|
||||||
|
proteinPercentage -= floor(self.getWaterPercentage() * 100) / 100
|
||||||
|
proteinPercentage -= floor((self.getBoneMass()/self.weight*100) * 100) / 100
|
||||||
|
|
||||||
|
return self.checkValueOverflow(proteinPercentage, 5, 32)
|
||||||
|
|
||||||
|
# Get body type (out of nine possible)
|
||||||
|
def getBodyType(self):
|
||||||
|
if self.getFatPercentage() > self.scales.getFatPercentageScale()[2]:
|
||||||
|
factor = 0
|
||||||
|
elif self.getFatPercentage() < self.scales.getFatPercentageScale()[1]:
|
||||||
|
factor = 2
|
||||||
|
else:
|
||||||
|
factor = 1
|
||||||
|
|
||||||
|
if self.getMuscleMass() > self.scales.getMuscleMassScale()[1]:
|
||||||
|
return 2 + (factor * 3)
|
||||||
|
elif self.getMuscleMass() < self.scales.getMuscleMassScale()[0]:
|
||||||
|
return (factor * 3)
|
||||||
|
else:
|
||||||
|
return 1 + (factor * 3)
|
||||||
|
|
||||||
|
# Get Metabolic Age
|
||||||
|
def getMetabolicAge(self):
|
||||||
|
if self.sex == 'female':
|
||||||
|
metabolicAge = (self.height * -1.1165) + (self.weight * 1.5784) + (self.age * 0.4615) + (self.impedance * 0.0415) + 83.2548
|
||||||
|
else:
|
||||||
|
metabolicAge = (self.height * -0.7471) + (self.weight * 0.9161) + (self.age * 0.4184) + (self.impedance * 0.0517) + 54.2267
|
||||||
|
return self.checkValueOverflow(metabolicAge, 15, 80)
|
155
src/body_scales.py
Normal file
155
src/body_scales.py
Normal file
@ -0,0 +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']
|
||||||
|
|
181
src/body_score.py
Normal file
181
src/body_score.py
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
|
||||||
|
# Reverse engineered from amazfit's app (also known as Mi Fit)
|
||||||
|
from body_scales import bodyScales
|
||||||
|
class bodyScore:
|
||||||
|
|
||||||
|
def __init__(self, age, sex, height, weight, bmi, bodyfat, muscle, water, visceral_fat, bone, basal_metabolism, protein):
|
||||||
|
self.age = age
|
||||||
|
self.sex = sex
|
||||||
|
self.height = height
|
||||||
|
self.weight = weight
|
||||||
|
self.bmi = bmi
|
||||||
|
self.bodyfat = bodyfat
|
||||||
|
self.muscle = muscle
|
||||||
|
self.water = water
|
||||||
|
self.visceral_fat = visceral_fat
|
||||||
|
self.bone = bone
|
||||||
|
self.basal_metabolism = basal_metabolism
|
||||||
|
self.protein = protein
|
||||||
|
self.scales = bodyScales(age, height, sex, weight)
|
||||||
|
|
||||||
|
def getBodyScore(self):
|
||||||
|
score = 100
|
||||||
|
score -= self.getBmiDeductScore()
|
||||||
|
score -= self.getBodyFatDeductScore()
|
||||||
|
score -= self.getMuscleDeductScore()
|
||||||
|
score -= self.getWaterDeductScore()
|
||||||
|
score -= self.getVisceralFatDeductScore()
|
||||||
|
score -= self.getBoneDeductScore()
|
||||||
|
score -= self.getBasalMetabolismDeductScore()
|
||||||
|
if self.protein:
|
||||||
|
score -= self.getProteinDeductScore()
|
||||||
|
return score
|
||||||
|
|
||||||
|
def getMalus(self, data, min_data, max_data, max_malus, min_malus):
|
||||||
|
result = ((data - max_data) / (min_data - max_data)) * float(max_malus - min_malus)
|
||||||
|
if result >= 0.0:
|
||||||
|
return result
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def getBmiDeductScore(self):
|
||||||
|
if not self.height >= 90:
|
||||||
|
# "BMI is not reasonable
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
bmi_low = 15.0
|
||||||
|
bmi_verylow = 14.0
|
||||||
|
bmi_normal = 18.5
|
||||||
|
bmi_overweight = 28.0
|
||||||
|
bmi_obese = 32.0
|
||||||
|
fat_scale = self.scales.getFatPercentageScale()
|
||||||
|
|
||||||
|
# Perfect range (bmi >= 18.5 and bodyfat not high for adults, bmi >= 15.0 for kids
|
||||||
|
if self.bmi >= 18.5 and self.age >= 18 and self.bodyfat < fat_scale[2]:
|
||||||
|
return 0.0
|
||||||
|
elif self.bmi >= bmi_verylow and self.age < 18 and self.bodyfat < fat_scale[2]:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
# Extremely skinny (bmi < 14)
|
||||||
|
elif self.bmi <= bmi_verylow:
|
||||||
|
return 30.0
|
||||||
|
# Too skinny (bmi between 14 and 15)
|
||||||
|
elif self.bmi > bmi_verylow and self.bmi < bmi_low:
|
||||||
|
return self.getMalus(self.bmi, bmi_verylow, bmi_low, 30, 15) + 15.0
|
||||||
|
# Skinny (for adults, between 15 and 18.5)
|
||||||
|
elif self.bmi >= bmi_low and self.bmi < bmi_normal and self.age >= 18:
|
||||||
|
return self.getMalus(self.bmi, 15.0, 18.5, 15, 5) + 5.0
|
||||||
|
|
||||||
|
# Normal or high bmi but too much bodyfat
|
||||||
|
elif ((self.bmi >= bmi_low and self.age < 18) or (self.bmi >= bmi_normal and self.age >= 18)) and self.bodyfat >= fat_scale[2]:
|
||||||
|
# Obese
|
||||||
|
if self.bmi >= bmi_obese:
|
||||||
|
return 10.0
|
||||||
|
# Overweight
|
||||||
|
if self.bmi > bmi_overweight:
|
||||||
|
return self.getMalus(self.bmi, 28.0, 25.0, 5, 10) + 5.0
|
||||||
|
else:
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def getBodyFatDeductScore(self):
|
||||||
|
scale = self.scales.getFatPercentageScale()
|
||||||
|
|
||||||
|
if self.sex == 'male':
|
||||||
|
best = scale[2] - 3.0
|
||||||
|
elif self.sex == 'female':
|
||||||
|
best = scale[2] - 2.0
|
||||||
|
|
||||||
|
# Slighly low in fat or low part or normal fat
|
||||||
|
if self.bodyfat >= scale[0] and self.bodyfat < best:
|
||||||
|
return 0.0
|
||||||
|
elif self.bodyfat >= scale[3]:
|
||||||
|
return 20.0
|
||||||
|
else:
|
||||||
|
# Sightly high body fat
|
||||||
|
if self.bodyfat < scale[3]:
|
||||||
|
return self.getMalus(self.bodyfat, scale[3], scale[2], 20, 10) + 10.0
|
||||||
|
|
||||||
|
# High part of normal fat
|
||||||
|
elif self.bodyfat <= scale[2]:
|
||||||
|
return self.getMalus(self.bodyfat, scale[2], best, 3, 9) + 3.0
|
||||||
|
|
||||||
|
# Very low in fat
|
||||||
|
elif self.bodyfat < scale[0]:
|
||||||
|
return self.getMalus(self.bodyfat, 1.0, scale[0], 3, 10) + 3.0
|
||||||
|
|
||||||
|
|
||||||
|
def getMuscleDeductScore(self):
|
||||||
|
scale = self.scales.getMuscleMassScale()
|
||||||
|
|
||||||
|
# For some reason, there's code to return self.calculate(muscle, normal[0], normal[0]+2.0, 3, 5) + 3.0
|
||||||
|
# if your muscle is between normal[0] and normal[0] + 2.0, but it's overwritten with 0.0 before return
|
||||||
|
if self.muscle >= scale[0]:
|
||||||
|
return 0.0
|
||||||
|
elif self.muscle < (scale[0] - 5.0):
|
||||||
|
return 10.0
|
||||||
|
else:
|
||||||
|
return self.getMalus(self.muscle, scale[0] - 5.0, scale[0], 10, 5) + 5.0
|
||||||
|
|
||||||
|
# No malus = normal or good; maximum malus (10.0) = less than normal-5.0;
|
||||||
|
# malus = between 5 and 10, on your water being between normal-5.0 and normal
|
||||||
|
def getWaterDeductScore(self):
|
||||||
|
scale = self.scales.getWaterPercentageScale()
|
||||||
|
|
||||||
|
if self.water >= scale[0]:
|
||||||
|
return 0.0
|
||||||
|
elif self.water <= (scale[0] - 5.0):
|
||||||
|
return 10.0
|
||||||
|
else:
|
||||||
|
return self.getMalus(self.water, scale[0] - 5.0, scale[0], 10, 5) + 5.0
|
||||||
|
|
||||||
|
# No malus = normal; maximum malus (15.0) = very high; malus = between 10 and 15
|
||||||
|
# with your visceral fat in your high range
|
||||||
|
def getVisceralFatDeductScore(self):
|
||||||
|
scale = self.scales.getVisceralFatScale()
|
||||||
|
|
||||||
|
if self.visceral_fat < scale[0]:
|
||||||
|
# For some reason, the original app would try to
|
||||||
|
# return 3.0 if vfat == 8 and 5.0 if vfat == 9
|
||||||
|
# but i's overwritten with 0.0 anyway before return
|
||||||
|
return 0.0
|
||||||
|
elif self.visceral_fat >= scale[1]:
|
||||||
|
return 15.0
|
||||||
|
else:
|
||||||
|
return self.getMalus(self.visceral_fat, scale[1], scale[0], 15, 10) + 10.0
|
||||||
|
|
||||||
|
def getBoneDeductScore(self):
|
||||||
|
scale = self.scales.getBoneMassScale()
|
||||||
|
|
||||||
|
if self.bone >= scale[0]:
|
||||||
|
return 0.0
|
||||||
|
elif self.bone <= (scale[0] - 0.3):
|
||||||
|
return 10.0
|
||||||
|
else:
|
||||||
|
return self.getMalus(self.bone, scale[0] - 0.3, scale[0], 10, 5) + 5.0
|
||||||
|
|
||||||
|
def getBasalMetabolismDeductScore(self):
|
||||||
|
# Get normal BMR
|
||||||
|
normal = self.scales.getBMRScale()[0]
|
||||||
|
|
||||||
|
if self.basal_metabolism >= normal:
|
||||||
|
return 0.0
|
||||||
|
elif self.basal_metabolism <= (normal - 300):
|
||||||
|
return 6.0
|
||||||
|
else:
|
||||||
|
# It's really + 5.0 in the app, but it's probably a mistake, should be 3.0
|
||||||
|
return self.getMalus(self.basal_metabolism, normal - 300, normal, 6, 3) + 5.0
|
||||||
|
|
||||||
|
|
||||||
|
# Get protein percentage malus
|
||||||
|
def getProteinDeductScore(self):
|
||||||
|
# low: 10,16; normal: 16,17
|
||||||
|
# Check limits
|
||||||
|
if self.protein > 17.0:
|
||||||
|
return 0.0
|
||||||
|
elif self.protein < 10.0:
|
||||||
|
return 10.0
|
||||||
|
else:
|
||||||
|
# Return values for low proteins or normal proteins
|
||||||
|
if self.protein <= 16.0:
|
||||||
|
return self.getMalus(self.protein, 10.0, 16.0, 10, 5) + 5.0
|
||||||
|
elif self.protein <= 17.0:
|
||||||
|
return self.getMalus(self.protein, 16.0, 17.0, 5, 3) + 3.0
|
2
src/requirements.txt
Normal file
2
src/requirements.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
bluepy==1.3.0
|
||||||
|
paho-mqtt==1.5.0
|
24
src/wrapper.sh
Normal file
24
src/wrapper.sh
Normal file
@ -0,0 +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 )"
|
||||||
|
python3 $MY_PWD/Xiaomi_Scale.py
|
Loading…
Reference in New Issue
Block a user