diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f987fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +# System Files +.DS_Store +Thumbs.db +Desktop.ini +desktop.ini + +# Docker exported images +*.docker diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..53b6cc6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +FROM python:3.8-rc-alpine + +WORKDIR /opt/miscale +COPY src /opt/miscale + +RUN apk update && \ + apk add --no-cache \ + dcron \ + bash \ + bash-doc \ + bash-completion \ + tar \ + linux-headers \ + gcc \ + make \ + glib-dev \ + alpine-sdk \ + && rm -rf /var/cache/apk/* + +RUN pip install -r requirements.txt + +RUN mkdir -p /var/log/cron \ + && mkdir -m 0644 -p /var/spool/cron/crontabs \ + && touch /var/log/cron/cron.log \ + && mkdir -m 0644 -p /etc/cron.d && \ + echo -e "*/5 * * * * python3 /opt/miscale/Xiaomi_Scale.py\n" >> /var/spool/cron/crontabs/root + +## Cleanup +RUN apk del alpine-sdk gcc make tar + +# Copy in docker scripts to root of container... (cron won't run unless it's run under bash/ash shell) +COPY dockerscripts/ / + +ENTRYPOINT ["/entrypoint.sh"] +CMD ["/cmd.sh"] + +# To test, run with the following: +# docker run --rm -it --privileged --net=host -e MISCALE_MAC=0C:95:41:C9:46:43 -e MQTT_HOST=10.16.10.4 mi-scale_mi-scale \ No newline at end of file diff --git a/README.md b/README.md index 95e771f..64b8fee 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,12 @@ Code to read weight measurements from [Mi Body Composition Scale](https://www.mi.com/global/mi-body-composition-scale/) (aka Xiaomi Mi Scale V2) -![Mi Scale](https://github.com/lolouk44/xiaomi_mi_scale/blob/master/Screenshots/Mi_Scale.png) +![Mi Scale](Screenshots/Mi_Scale.png) Note: Framework is present to also read from Xiaomi Scale V1, although I do not own one to test so code has not been maintained -## Setup: +## Getting the Mac Address of your Scale: + 1. Retrieve the scale's MAC Address (you can identify your scale by looking for `MIBCS` entries) using this command: ``` $ sudo hcitool lescan @@ -15,39 +16,52 @@ F8:04:33:AF:AB:A2 [TV] UE48JU6580 C4:D3:8C:12:4C:57 MIBCS [...] ``` -1. Copy all files -1. Open `Xiaomi_Scale.py` -1. Assign Scale's MAC address to variable `MISCALE_MAC` -1. Edit MQTT Credentials -1. Edit user logic/data on lines 117-131 +1. Note down your `MIBCS` mac address - we will need to use this as part of your configuration... -## How to use? -- Must be executed with Python 3 else body measurements are incorrect. -- Must be executed as root, therefore best to schedule via crontab every 5 min (so as not to drain the battery): +## Setup & Configuration: +### Running script with Docker: + +1. Open `docker-compose.yml` and edit the environment to suit your configuration... +1. Stand up the container - `docker-compose up -d` + +### Running script directly on your host system: + +1. Install python requirements (pip3 install -r requirements.txt) +1. Open `wrapper.sh` and configure your environment variables to suit your setup. +1. Add a cron-tab entry to wrapper like so: + +```sh +*/5 * * * * bash /path/to/wrapper.sh +``` + +**NOTE**: It's best to schedule via crontab at most, every 5 min (so as not to drain the battery on your scale): ``` */5 * * * * python3 /path-to-script/Xiaomi_Scale.py ``` ## Home-Assistant Setup: -Under the `sensor` block, enter as many blocks as users setup on lines 117-131 in `Xiaomi_Scale.py`. -``` +Under the `sensor` block, enter as many blocks as users configured in your environment variables: + +```yaml - platform: mqtt - name: "Lolo Weight" - state_topic: "lolo/weight" + name: "Example Name Weight" + state_topic: "miScale/USERS_NAME/weight" value_template: "{{ value_json['Weight'] }}" unit_of_measurement: "kg" - json_attributes_topic: "lolo/weight" + json_attributes_topic: "miScale/USERS_NAME/weight" icon: mdi:scale-bathroom - platform: mqtt - name: "Lolo BMI" - state_topic: "lolo/weight" + name: "Example Name BMI" + state_topic: "miScale/USERS_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) +``` + +![Mi Scale](Screenshots/HA_Lovelace_Card.png) + +![Mi Scale](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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4a6ae71 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3' +services: + + mi-scale: + container_name: mi-scale + build: . + restart: always + + network_mode: host + privileged: true + + environment: + MISCALE_MAC: 00:00:00:00:00:00 # Mac address of your scale + MQTT_HOST: 127.0.0.1 # MQTT Server (defaults to 127.0.0.1) + MQTT_PREFIX: miScale + # MQTT_USERNAME: # Username for MQTT server (comment out if not required) + # MQTT_PASSWORD: # Password for MQTT (comment out if not required) + # MQTT_PORT: # Defaults to 1883 + # MQTT_TIMEOUT: 30 # Defaults to 60 + + # Auto-gender selection/config -- This is used to create the calculations such as BMI, Water/Bone Mass etc... + # Multi user possible as long as weitghs do not overlap! + + USER1_GT: 70 # If the weight is greater than this number, we'll assume that we're weighing User #1 + USER1_SEX: male + USER1_NAME: Jo # Name of the user + USER1_HEIGHT: 175 # Height (in cm) of the user + USER1_DOB: "1990-01-01" # DOB (in yyyy-mm-dd format) + + USER2_LT: 35 # If the weight is less than this number, we'll assume that we're weighing User #2 + USER2_SEX: female + USER2_NAME: Serena # Name of the user + USER2_HEIGHT: 95 # Height (in cm) of the user + USER2_DOB: "1990-01-01" # DOB (in yyyy-mm-dd format) + + USER3_SEX: female + USER3_NAME: Missy # Name of the user + USER3_HEIGHT: 150 # Height (in cm) of the user + USER3_DOB: "1990-01-01" # DOB (in yyyy-mm-dd format) diff --git a/dockerscripts/cmd.sh b/dockerscripts/cmd.sh new file mode 100755 index 0000000..2b1ab04 --- /dev/null +++ b/dockerscripts/cmd.sh @@ -0,0 +1,10 @@ +set -e + +if [ ! -z "$CRON_TAIL" ] +then + # crond running in background and log file reading every second by tail to STDOUT + crond -s /var/spool/cron/crontabs -b -L /var/log/cron/cron.log "$@" && tail -f /var/log/cron/cron.log +else + # crond running in foreground. log files can be retrive from /var/log/cron mount point + crond -s /var/spool/cron/crontabs -f -L /var/log/cron/cron.log "$@" +fi \ No newline at end of file diff --git a/dockerscripts/entrypoint.sh b/dockerscripts/entrypoint.sh new file mode 100755 index 0000000..84e85f9 --- /dev/null +++ b/dockerscripts/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +rm -rf /var/spool/cron/crontabs && mkdir -m 0644 -p /var/spool/cron/crontabs + +[ "$(ls -A /etc/cron.d)" ] && cp -f /etc/cron.d/* /var/spool/cron/crontabs/ || true +[ ! -z "$CRON_STRINGS" ] && echo -e "$CRON_STRINGS\n" > /var/spool/cron/crontabs/CRON_STRINGS + +chmod -R 0644 /var/spool/cron/crontabs + +exec "$@" diff --git a/Xiaomi_Scale.py b/src/Xiaomi_Scale.py similarity index 69% rename from Xiaomi_Scale.py rename to src/Xiaomi_Scale.py index 21d89b3..c07b586 100644 --- a/Xiaomi_Scale.py +++ b/src/Xiaomi_Scale.py @@ -8,7 +8,7 @@ # # Thanks to @syssi (https://gist.github.com/syssi/4108a54877406dc231d95514e538bde9) and @prototux (https://github.com/wiecosystem/Bluetooth) for their initial code # -# Make sure you edit MQTT credentials below and user logic/data on lines 117-131 +# Make sure you set your MQTT credentials below and user logic/data through the environment variables # ############################################################################################# @@ -28,12 +28,33 @@ from datetime import datetime import Xiaomi_Scale_Body_Metrics -MISCALE_MAC = 'REDACTED' -MQTT_USERNAME = 'REDACTED' -MQTT_PASSWORD = 'REDACTED' -MQTT_HOST = 'REDACTED' -MQTT_PORT = 1883 -MQTT_TIMEOUT = 60 +# Configuraiton... +MISCALE_MAC = os.getenv('MISCALE_MAC', '') +MQTT_USERNAME = os.getenv('MQTT_USERNAME', '') +MQTT_PASSWORD = os.getenv('MQTT_PASSWORD', '') +MQTT_HOST = os.getenv('MQTT_HOST', '127.0.0.1') +MQTT_PORT = os.getenv('MQTT_PORT', 1883) +MQTT_TIMEOUT = os.getenv('MQTT_TIMEOUT', 60) +MQTT_PREFIX = os.getenv('MQTT_PREFIX', '') + +# 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) class ScanProcessor(): @@ -114,21 +135,21 @@ class ScanProcessor(): def _publish(self, weight, unit, mitdatetime, miimpedance): if not self.connected: raise Exception('not connected to MQTT server') - if int(weight) > 72: - user="lolo" - height=175 - age=self.GetAge("1900-01-01") - sex="male" - elif int(weight) < 50: - user="kiaan" - height=103 - age=self.GetAge("1900-01-01") - sex="male" + 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 = "div" - height=170 - age=self.GetAge("1900-01-01") - sex="female" + 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) + '"' @@ -144,12 +165,12 @@ class ScanProcessor(): message += ',"Bone Mass":"' + "{:.2f}".format(lib.getBoneMass()) + '"' message += ',"Muscle Mass":"' + "{:.2f}".format(lib.getMuscleMass()) + '"' message += ',"Protein":"' + "{:.2f}".format(lib.getProteinPercentage()) + '"' - self.mqtt_client.publish(user, weight, qos=1, retain=True) + self.mqtt_client.publish(MQTT_PREFIX + '/' + user, weight, qos=1, retain=True) message += ',"TimeStamp":"' + mitdatetime + '"' message += '}' - self.mqtt_client.publish(user+'/weight', message, qos=1, retain=True) - print('\tSent data to topic %s: %s' % (user+'/weight', message)) + self.mqtt_client.publish(MQTT_PREFIX + '/' + user + '/weight', message, qos=1, retain=True) + print('\tSent data to topic %s: %s' % (MQTT_PREFIX + '/' + user + '/weight', message)) def main(): diff --git a/Xiaomi_Scale_Body_Metrics.py b/src/Xiaomi_Scale_Body_Metrics.py similarity index 100% rename from Xiaomi_Scale_Body_Metrics.py rename to src/Xiaomi_Scale_Body_Metrics.py diff --git a/src/requirements.txt b/src/requirements.txt new file mode 100644 index 0000000..852e9a8 --- /dev/null +++ b/src/requirements.txt @@ -0,0 +1,2 @@ +bluepy==1.3.0 +paho-mqtt==1.4.0 \ No newline at end of file diff --git a/src/wrapper.sh b/src/wrapper.sh new file mode 100644 index 0000000..d608b47 --- /dev/null +++ b/src/wrapper.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +export MISCALE_MAC=00:00:00:00:00:00 # Mac address of your scale +export MQTT_PREFIX=miScale +#export MQTT_HOST= # MQTT Server (defaults to 127.0.0.1) +#export MQTT_USERNAME= # Username for MQTT server (comment out if not required) +#export MQTT_PASSWORD= # Password for MQTT (comment out if not required) +#export MQTT_PORT= # Defaults to 1883 +#export MQTT_TIMEOUT=30 # Defaults to 60 + +# Auto-gender selection/config -- This is used to create the calculations such as BMI, Water/Bone Mass etc... +# Multi user possible as long as weitghs do not overlap! + +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