From 1cf407f043e15606278061560a00ae315f9f2118 Mon Sep 17 00:00:00 2001 From: Lolouk44 Date: Mon, 8 Mar 2021 22:01:30 +0000 Subject: [PATCH] vscode sync --- .github/ISSUE_TEMPLATE/bug_report.md | 64 +- .github/ISSUE_TEMPLATE/feature_request.md | 40 +- .github/workflows/main.yml | 70 +-- LICENSE | 42 +- README.md | 282 ++++----- src/Xiaomi_Scale.py | 730 +++++++++++----------- src/body_scales.py | 310 ++++----- src/wrapper.sh | 64 +- 8 files changed, 801 insertions(+), 801 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index cdb6951..fd2f03f 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -1,32 +1,32 @@ ---- -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 # - -**Desktop/Server (please complete the following information):** - - Docker or manually ran after a Git Clone? - - 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. +--- +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 # + +**Desktop/Server (please complete the following information):** + - Docker or manually ran after a Git Clone? + - 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. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 11fc491..73987ed 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +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. +--- +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. diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4b06265..937eae8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,35 +1,35 @@ -name: Publish Docker image -on: - release: - types: [published] - -jobs: - push_to_registry: - name: Push Docker image to Docker Hub - runs-on: ubuntu-latest - steps: - - name: Check out the repo - uses: actions/checkout@v2 - - - name: Set up Docker Buildx - id: buildx - uses: docker/setup-buildx-action@v1 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v1 - - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build image - run: | - docker buildx build --no-cache --push \ - --tag lolouk44/xiaomi-mi-scale:${{ github.event.release.tag_name }} \ - --tag lolouk44/xiaomi-mi-scale:latest \ - --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 . - - - name: Image digest - run: echo ${{ steps.docker_build.outputs.digest }} +name: Publish Docker image +on: + release: + types: [published] + +jobs: + push_to_registry: + name: Push Docker image to Docker Hub + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build image + run: | + docker buildx build --no-cache --push \ + --tag lolouk44/xiaomi-mi-scale:${{ github.event.release.tag_name }} \ + --tag lolouk44/xiaomi-mi-scale:latest \ + --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 . + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/LICENSE b/LICENSE index 3c36ec9..958e460 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +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. +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. diff --git a/README.md b/README.md index e9fe394..725c171 100644 --- a/README.md +++ b/README.md @@ -1,141 +1,141 @@ -# Xiaomi Mi Scale - -Code to read weight measurements from Xiaomi Body Scales. - -## BREAKING CHANGE: -Please note there was a breaking change in 0.1.8. The MQTT message json attributes are now in lower snake_case to be compliant with Home-Assistant Attributes. -This means Home-Assistant sensor configuration needs to be adjusted. -For example -`value_template: "{{ value_json['Weight'] }}"` -Needs to be replaced with -`value_template: "{{ value_json['weight'] }}"` -(note the lowercase `w` in `weight`) - -## Supported Scales: -Name | Model | Picture ---- | --- | :---: -[Mi Smart Scale 2](https://www.mi.com/global/scale)                                                                                               | XMTZCO1HM, XMTZC04HM | ![Mi Scale_2](Screenshots/Mi_Smart_Scale_2_Thumb.png) -[Mi Body Composition Scale](https://www.mi.com/global/mi-body-composition-scale/) | XMTZC02HM | ![Mi Scale](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](Screenshots/Mi_Body_Composition_Scale_2_Thumb.png) - - -## Home Assistant Add-On: -If using Home Assistant (formerly known as hass.io), try instead the [Xiaomi Mi Scale Add-On for Home Assistant](https://github.com/lolouk44/hassio-addons/tree/master/mi-scale) based on this repository. - -## Getting the Mac Address of your Scale: - -1. Retrieve the scale's MAC Address from the Xiaomi Mi Fit App: - -![MAC Address](Screenshots/MAC_Address.png) - -## Setup & Configuration: -### Running script with Docker: - -1. Supported platforms: - 1. linux/386 - 1. linux/amd64 - 1. linux/arm32v6 - 1. linux/arm32v7 - 1. linux/arm64v8 -1. Open `docker-compose.yml` (see below) and edit the environment to suit your configuration... -1. Stand up the container - `docker-compose up -d` - -### docker-compose: -```yaml -version: '3' -services: - - mi-scale: - image: lolouk44/xiaomi-mi-scale:latest - container_name: mi-scale - restart: always - - network_mode: host - privileged: true - - environment: - - HCI_DEV=hci0 # Bluetooth hci device to use. Defaults to hci0 - - 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 Topic Prefix. Defaults to 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_TLS_CACERTS= # MQTT TLS connection: directory with CA certificate(s) that signed MQTT Server's TLS certificate, defaults to None (= no TLS connection) - - MQTT_TLS_INSECURE= # MQTT TLS connection: don't verify hostname in TLS certificate, defaults to None (= always check hostname) - - TIME_INTERVAL=30 # Time in sec between each query to the scale, to allow other applications to use the Bluetooth module. Defaults to 30 - - MQTT_DISCOVERY=true # Home Assistant Discovery (true/false), defaults to true - - MQTT_DISCOVERY_PREFIX= # Home Assistant Discovery Prefix, 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! - - - # Here is the logic used to assign a measured weight to a user: - # if [measured value in kg] is greater than USER1_GT, assign it to USER1 - # else if [measured value in kg] is less than USER2_LT, assign it to USER2 - # else assign it to USER3 (e.g. USER2_LT < [measured value in kg] < USER1_GT) - - - USER1_GT=70 # If the weight (in kg) is greater than this number, we'll assume that we're weighing User #1 - - USER1_SEX=male # male / female - - 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 (in kg) is less than this number, we'll assume that we're weighing User #2 - - USER2_SEX=female # male / 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 # male / 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) -``` - - -### Running script directly on your host system (if your platform is not listed/supported): - -**Note: Python 3.6 or higher is required to run the script manually** -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 -@reboot bash /path/to/wrapper.sh -``` - -**NOTE**: Although once started the script runs continuously, it may take a few seconds for the data to be retrieved, computed and sent via mqtt. - -## 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 - unit_of_measurement: "kg/m2" - -``` - -![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 - -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), [@AiiR42](https://github.com/AiiR42) for their PRs and collaboration +# Xiaomi Mi Scale + +Code to read weight measurements from Xiaomi Body Scales. + +## BREAKING CHANGE: +Please note there was a breaking change in 0.1.8. The MQTT message json attributes are now in lower snake_case to be compliant with Home-Assistant Attributes. +This means Home-Assistant sensor configuration needs to be adjusted. +For example +`value_template: "{{ value_json['Weight'] }}"` +Needs to be replaced with +`value_template: "{{ value_json['weight'] }}"` +(note the lowercase `w` in `weight`) + +## Supported Scales: +Name | Model | Picture +--- | --- | :---: +[Mi Smart Scale 2](https://www.mi.com/global/scale)                                                                                               | XMTZCO1HM, XMTZC04HM | ![Mi Scale_2](Screenshots/Mi_Smart_Scale_2_Thumb.png) +[Mi Body Composition Scale](https://www.mi.com/global/mi-body-composition-scale/) | XMTZC02HM | ![Mi Scale](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](Screenshots/Mi_Body_Composition_Scale_2_Thumb.png) + + +## Home Assistant Add-On: +If using Home Assistant (formerly known as hass.io), try instead the [Xiaomi Mi Scale Add-On for Home Assistant](https://github.com/lolouk44/hassio-addons/tree/master/mi-scale) based on this repository. + +## Getting the Mac Address of your Scale: + +1. Retrieve the scale's MAC Address from the Xiaomi Mi Fit App: + +![MAC Address](Screenshots/MAC_Address.png) + +## Setup & Configuration: +### Running script with Docker: + +1. Supported platforms: + 1. linux/386 + 1. linux/amd64 + 1. linux/arm32v6 + 1. linux/arm32v7 + 1. linux/arm64v8 +1. Open `docker-compose.yml` (see below) and edit the environment to suit your configuration... +1. Stand up the container - `docker-compose up -d` + +### docker-compose: +```yaml +version: '3' +services: + + mi-scale: + image: lolouk44/xiaomi-mi-scale:latest + container_name: mi-scale + restart: always + + network_mode: host + privileged: true + + environment: + - HCI_DEV=hci0 # Bluetooth hci device to use. Defaults to hci0 + - 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 Topic Prefix. Defaults to 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_TLS_CACERTS= # MQTT TLS connection: directory with CA certificate(s) that signed MQTT Server's TLS certificate, defaults to None (= no TLS connection) + - MQTT_TLS_INSECURE= # MQTT TLS connection: don't verify hostname in TLS certificate, defaults to None (= always check hostname) + - TIME_INTERVAL=30 # Time in sec between each query to the scale, to allow other applications to use the Bluetooth module. Defaults to 30 + - MQTT_DISCOVERY=true # Home Assistant Discovery (true/false), defaults to true + - MQTT_DISCOVERY_PREFIX= # Home Assistant Discovery Prefix, 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! + + + # Here is the logic used to assign a measured weight to a user: + # if [measured value in kg] is greater than USER1_GT, assign it to USER1 + # else if [measured value in kg] is less than USER2_LT, assign it to USER2 + # else assign it to USER3 (e.g. USER2_LT < [measured value in kg] < USER1_GT) + + - USER1_GT=70 # If the weight (in kg) is greater than this number, we'll assume that we're weighing User #1 + - USER1_SEX=male # male / female + - 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 (in kg) is less than this number, we'll assume that we're weighing User #2 + - USER2_SEX=female # male / 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 # male / 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) +``` + + +### Running script directly on your host system (if your platform is not listed/supported): + +**Note: Python 3.6 or higher is required to run the script manually** +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 +@reboot bash /path/to/wrapper.sh +``` + +**NOTE**: Although once started the script runs continuously, it may take a few seconds for the data to be retrieved, computed and sent via mqtt. + +## 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 + unit_of_measurement: "kg/m2" + +``` + +![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 + +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), [@AiiR42](https://github.com/AiiR42) for their PRs and collaboration diff --git a/src/Xiaomi_Scale.py b/src/Xiaomi_Scale.py index 10e790c..eeb802e 100644 --- a/src/Xiaomi_Scale.py +++ b/src/Xiaomi_Scale.py @@ -1,365 +1,365 @@ -#!/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 = "username" - 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_TLS_CACERTS = data["MQTT_TLS_CACERTS"] - except: - MQTT_TLS_CACERTS = None - pass - try: - MQTT_TLS_INSECURE = data["MQTT_TLS_INSECURE"] - except: - MQTT_TLS_INSECURE = None - 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_TLS_CACERTS = os.getenv('MQTT_TLS_CACERTS', None) - MQTT_TLS_INSECURE = os.getenv('MQTT_TLS_INSECURE', None) - MQTT_PREFIX = os.getenv('MQTT_PREFIX', 'miscale') - TIME_INTERVAL = int(os.getenv('TIME_INTERVAL', 30)) - MQTT_DISCOVERY = os.getenv('MQTT_DISCOVERY',True) - if MQTT_DISCOVERY.lower() in ['true', '1', 'y', 'yes']: - MQTT_DISCOVERY = True - else: - MQTT_DISCOVERY = False - 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") - -if MQTT_TLS_CACERTS is None: - MQTT_TLS = None -else: - MQTT_TLS = {'ca_certs':MQTT_TLS_CACERTS, 'insecure':MQTT_TLS_INSECURE} - -OLD_MEASURE = '' - -def discovery(): - for MQTTUser in (USER1_NAME,USER2_NAME,USER3_NAME): - message = '{"name": "' + MQTTUser + ' Weight",' - message+= '"state_topic": "' + MQTT_PREFIX + '/' + MQTTUser + '/weight","value_template": "{{ value_json.weight }}",' - message+= '"json_attributes_topic": "' + MQTT_PREFIX + '/' + 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}, - tls=MQTT_TLS - ) - 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', 'a3')): 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.now()), "", "") - 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.now()), hasImpedance, miimpedance) - OLD_MEASURE = round(measured, 2) + int(miimpedance) - - - def _publish(self, weight, unit, mitdatetime, hasImpedance, miimpedance): - if unit == "lbs": calcweight = round(weight * 0.4536, 2) - if unit == "jin": calcweight = round(weight * 0.5, 2) - if unit == "kg": calcweight = weight - if int(calcweight) > USER1_GT: - user = USER1_NAME - height = USER1_HEIGHT - age = self.GetAge(USER1_DOB) - sex = USER1_SEX - elif int(calcweight) < 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(calcweight, height, age, sex, 0) - message = '{' - message += '"weight":' + "{:.2f}".format(weight) - message += ',"weight_unit":"' + str(unit) + '"' - 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(calcweight, 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}, - tls=MQTT_TLS - ) - 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 hci' + HCI_DEV + ' down' - ps = subprocess.Popen(cmd, shell=True) - time.sleep(1) - cmd = 'hciconfig hci' + HCI_DEV + ' up' - 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 = "username" + 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_TLS_CACERTS = data["MQTT_TLS_CACERTS"] + except: + MQTT_TLS_CACERTS = None + pass + try: + MQTT_TLS_INSECURE = data["MQTT_TLS_INSECURE"] + except: + MQTT_TLS_INSECURE = None + 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_TLS_CACERTS = os.getenv('MQTT_TLS_CACERTS', None) + MQTT_TLS_INSECURE = os.getenv('MQTT_TLS_INSECURE', None) + MQTT_PREFIX = os.getenv('MQTT_PREFIX', 'miscale') + TIME_INTERVAL = int(os.getenv('TIME_INTERVAL', 30)) + MQTT_DISCOVERY = os.getenv('MQTT_DISCOVERY',True) + if MQTT_DISCOVERY.lower() in ['true', '1', 'y', 'yes']: + MQTT_DISCOVERY = True + else: + MQTT_DISCOVERY = False + 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") + +if MQTT_TLS_CACERTS is None: + MQTT_TLS = None +else: + MQTT_TLS = {'ca_certs':MQTT_TLS_CACERTS, 'insecure':MQTT_TLS_INSECURE} + +OLD_MEASURE = '' + +def discovery(): + for MQTTUser in (USER1_NAME,USER2_NAME,USER3_NAME): + message = '{"name": "' + MQTTUser + ' Weight",' + message+= '"state_topic": "' + MQTT_PREFIX + '/' + MQTTUser + '/weight","value_template": "{{ value_json.weight }}",' + message+= '"json_attributes_topic": "' + MQTT_PREFIX + '/' + 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}, + tls=MQTT_TLS + ) + 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', 'a3')): 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.now()), "", "") + 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.now()), hasImpedance, miimpedance) + OLD_MEASURE = round(measured, 2) + int(miimpedance) + + + def _publish(self, weight, unit, mitdatetime, hasImpedance, miimpedance): + if unit == "lbs": calcweight = round(weight * 0.4536, 2) + if unit == "jin": calcweight = round(weight * 0.5, 2) + if unit == "kg": calcweight = weight + if int(calcweight) > USER1_GT: + user = USER1_NAME + height = USER1_HEIGHT + age = self.GetAge(USER1_DOB) + sex = USER1_SEX + elif int(calcweight) < 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(calcweight, height, age, sex, 0) + message = '{' + message += '"weight":' + "{:.2f}".format(weight) + message += ',"weight_unit":"' + str(unit) + '"' + 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(calcweight, 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}, + tls=MQTT_TLS + ) + 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 hci' + HCI_DEV + ' down' + ps = subprocess.Popen(cmd, shell=True) + time.sleep(1) + cmd = 'hciconfig hci' + HCI_DEV + ' up' + 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/body_scales.py b/src/body_scales.py index 33a7c16..2f265c2 100644 --- a/src/body_scales.py +++ b/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/wrapper.sh b/src/wrapper.sh index c8f556d..17f7c00 100644 --- a/src/wrapper.sh +++ b/src/wrapper.sh @@ -1,33 +1,33 @@ -#!/bin/bash - -sleep 60 # Give the system time after a reboot to connect to WiFi before continuing -export MISCALE_MAC=00:00:00:00:00:00 # Mac address of your scale -export HCI_DEV=hci0 # Bluetooth hci device to use -export MQTT_HOST=127.0.0.1 # MQTT Server (defaults to 127.0.0.1) -export MQTT_PREFIX=miscale # MQTT Topic Prefix. Defaults to miscale -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=1883 # Defaults to 1883 -export TIME_INTERVAL=30 # Time in sec between each query to the scale, to allow other applications to use the Bluetooth module. Defaults to 30 -export MQTT_DISCOVERY=true # Home Assistant Discovery (true/false), defaults to true -export MQTT_DISCOVERY_PREFIX= # Home Assistant Discovery Prefix, defaults to homeassistant - -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 + +sleep 60 # Give the system time after a reboot to connect to WiFi before continuing +export MISCALE_MAC=00:00:00:00:00:00 # Mac address of your scale +export HCI_DEV=hci0 # Bluetooth hci device to use +export MQTT_HOST=127.0.0.1 # MQTT Server (defaults to 127.0.0.1) +export MQTT_PREFIX=miscale # MQTT Topic Prefix. Defaults to miscale +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=1883 # Defaults to 1883 +export TIME_INTERVAL=30 # Time in sec between each query to the scale, to allow other applications to use the Bluetooth module. Defaults to 30 +export MQTT_DISCOVERY=true # Home Assistant Discovery (true/false), defaults to true +export MQTT_DISCOVERY_PREFIX= # Home Assistant Discovery Prefix, defaults to homeassistant + +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