├── mi-scale ├── src │ ├── requirements.txt │ ├── body_scales.py │ ├── body_score.py │ ├── Xiaomi_Scale_Body_Metrics.py │ └── Xiaomi_Scale.py ├── dockerscripts │ ├── entrypoint.sh │ └── cmd.sh ├── icon.png ├── logo.png ├── Dockerfile ├── README.md ├── config.json ├── apparmor.txt ├── CHANGELOG.md └── DOCS.md ├── repository.json ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ └── main.yml └── LICENSE /mi-scale/src/requirements.txt: -------------------------------------------------------------------------------- 1 | bleak==0.18.0 2 | paho-mqtt==1.5.0 -------------------------------------------------------------------------------- /mi-scale/dockerscripts/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | exec "$@" -------------------------------------------------------------------------------- /mi-scale/dockerscripts/cmd.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | python3 -u /opt/miscale/Xiaomi_Scale.py 3 | -------------------------------------------------------------------------------- /mi-scale/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lolouk44/hassio-addons/HEAD/mi-scale/icon.png -------------------------------------------------------------------------------- /mi-scale/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lolouk44/hassio-addons/HEAD/mi-scale/logo.png -------------------------------------------------------------------------------- /repository.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Lolouk44 Add-Ons", 3 | "url": "https://github.com/lolouk44/hassio-addons", 4 | "maintainer": "lolouk44" 5 | } -------------------------------------------------------------------------------- /mi-scale/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.9-slim 2 | LABEL io.hass.version="0.3.6" io.hass.type="addon" io.hass.arch="armhf|aarch64|i386|amd64" 3 | WORKDIR /opt/miscale 4 | COPY src /opt/miscale 5 | 6 | RUN apt-get update && apt-get install --no-install-recommends -y \ 7 | build-essential \ 8 | bluez \ 9 | python3-pip \ 10 | libglib2.0-dev && \ 11 | rm -rf /var/lib/apt/lists/* 12 | 13 | RUN pip install -r requirements.txt 14 | 15 | # Copy in docker scripts to root of container... 16 | COPY dockerscripts/ / 17 | 18 | RUN chmod +x /entrypoint.sh 19 | RUN chmod +x /cmd.sh 20 | ENTRYPOINT ["/entrypoint.sh"] 21 | CMD ["/cmd.sh"] 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Select Add-On (place a lowercase `x` for the relevant add-on)** 11 | - [ ] Xiaomi Mi Scale 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Select Add-On (place a lowercase `x` for the relevant add-on)** 11 | - [ ] Xiaomi Mi Scale 12 | 13 | **Describe the bug** 14 | A clear and concise description of what the bug is. 15 | 16 | **To Reproduce** 17 | Steps to reproduce the behaviour, including error message if any. 18 | 19 | **Expected behaviour** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Scale (please complete the following information):** 26 | - Name 27 | - Model # 28 | 29 | **Device running Home Assistant (please complete the following information):** 30 | - Device used to run the Script/Container [e.g. Raspberry Pi, NUC] 31 | - Bluetooth device used [e.g. Built-in, USB Dongle] 32 | 33 | **Home Assistant Version:** 34 | e.g. 2010.01.1 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 lolouk44 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /mi-scale/README.md: -------------------------------------------------------------------------------- 1 | [![version](https://img.shields.io/github/v/release/lolouk44/hassio-addons)](https://github.com/lolouk44/hassio-addons/releases) 2 | [![ha_badge](https://img.shields.io/badge/Home%20Assistant-Add%20On-blue.svg)](https://www.home-assistant.io/) 3 | # Home Assistant Add-on: Xiaomi Mi Scale 4 | 5 | [aarch64-shield]: https://img.shields.io/badge/aarch64-yes-green.svg 6 | [amd64-shield]: https://img.shields.io/badge/amd64-yes-green.svg 7 | [armv6-shield]: https://img.shields.io/badge/armv6-yes-green.svg 8 | [armv7-shield]: https://img.shields.io/badge/armv7-yes-green.svg 9 | [i386-shield]: https://img.shields.io/badge/i386-yes-green.svg 10 | ![aarch64-shield] 11 | ![amd64-shield] 12 | ![armv6-shield] 13 | ![armv7-shield] 14 | ![i386-shield] 15 | 16 | Reads weight measurements from Xiaomi Body Scales via Bluetooth. See [Documentation](https://github.com/lolouk44/hassio-addons/tree/master/mi-scale/DOCS.md) for installation instructions 17 | 18 | ## About 19 | This Add-on for [Home Assistant](https://www.home-assistant.io/) connects via Bluetooth Low Energy (BLE) to Xiaomi Bathroom Scales to read weight measurements from the device, including Body Mass Index (BMI), basal metabolism, visceral fat, lean body mass, body fat, water amount, bone mass, muscle mass, protein amount, body type, weight category, and metabolic age. 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | on: 3 | release: 4 | types: [published] 5 | 6 | jobs: 7 | push_to_registry: 8 | name: Push Docker image to Docker Hub 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Check out the repo 12 | uses: actions/checkout@v2 13 | 14 | - name: Set up Docker Buildx 15 | id: buildx 16 | uses: docker/setup-buildx-action@v1 17 | 18 | - name: Set up QEMU 19 | uses: docker/setup-qemu-action@v1 20 | 21 | - name: Login to DockerHub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ secrets.DOCKERHUB_USERNAME }} 25 | password: ${{ secrets.DOCKERHUB_TOKEN }} 26 | 27 | - name: Build Pre-Release image 28 | if: "github.event.release.prerelease" 29 | run: | 30 | docker buildx build --no-cache --push \ 31 | --tag lolouk44/xiaomi-mi-scale-ha-add-on:${{ github.event.release.tag_name }} \ 32 | --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 ./mi-scale/ 33 | 34 | - name: Build Release image 35 | if: "!github.event.release.prerelease" 36 | run: | 37 | docker buildx build --no-cache --push \ 38 | --tag lolouk44/xiaomi-mi-scale-ha-add-on:${{ github.event.release.tag_name }} \ 39 | --tag lolouk44/xiaomi-mi-scale-ha-add-on:latest \ 40 | --platform linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8 ./mi-scale/ 41 | 42 | - name: Image digest 43 | run: echo ${{ steps.docker_build.outputs.digest }} 44 | -------------------------------------------------------------------------------- /mi-scale/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Xiaomi Mi Scale", 3 | "version": "0.3.6", 4 | "slug": "xiaomi_mi_scale", 5 | "description": "Read weight measurements from a Xiaomi bathroom scale via Bluetooth (BLE)", 6 | "url": "https://github.com/lolouk44/xiaomi_mi_scale_ha_add_on", 7 | "image": "lolouk44/xiaomi-mi-scale-ha-add-on", 8 | "arch": ["armhf", "armv7", "aarch64", "amd64", "i386"], 9 | "startup": "application", 10 | "boot": "auto", 11 | "host_dbus": true, 12 | "panel_admin": false, 13 | "host_network": true, 14 | "apparmor": true, 15 | "privileged": ["NET_ADMIN", "SYS_ADMIN"], 16 | 17 | "options": { 18 | "HCI_DEV": "hci0", 19 | "BLUEPY_PASSIVE_SCAN": false, 20 | "MISCALE_MAC": "00:00:00:00:00:00", 21 | "MQTT_PREFIX": "miscale", 22 | "MQTT_HOST": "127.0.0.1", 23 | "MQTT_USERNAME": "user", 24 | "MQTT_PASSWORD": "passwd", 25 | "MQTT_PORT": 1883, 26 | "MQTT_RETAIN": true, 27 | "MQTT_DISCOVERY": true, 28 | "MQTT_DISCOVERY_PREFIX": "homeassistant", 29 | "MQTT_TLS_CACERTS": "Path to CA Cert File", 30 | "MQTT_TLS_INSECURE": false, 31 | "DEBUG_LEVEL": "error", 32 | 33 | "USERS": [ 34 | { 35 | "NAME": "Bob", 36 | "SEX": "male", 37 | "GT": 60, 38 | "LT": 80, 39 | "HEIGHT": 180, 40 | "DOB": "1980-01-01" 41 | }, 42 | { 43 | "NAME": "Mary", 44 | "SEX": "female", 45 | "GT": 40, 46 | "LT": 60, 47 | "HEIGHT": 160, 48 | "DOB": "1990-01-01" 49 | }, 50 | { 51 | "NAME": "Timmy", 52 | "SEX": "male", 53 | "GT": 20, 54 | "LT": 40, 55 | "HEIGHT": 120, 56 | "DOB": "2010-01-01" 57 | }, 58 | { 59 | "NAME": "Baby Sally", 60 | "SEX": "female", 61 | "GT": 1, 62 | "LT": 20, 63 | "HEIGHT": 50, 64 | "DOB": "2020-01-01" 65 | } 66 | ] 67 | 68 | }, 69 | "schema": { 70 | "HCI_DEV": "str?", 71 | "BLUEPY_PASSIVE_SCAN": "bool?", 72 | "MISCALE_MAC": "str", 73 | "MQTT_PREFIX": "str?", 74 | "MQTT_HOST": "str", 75 | "MQTT_USERNAME": "str?", 76 | "MQTT_PASSWORD": "str?", 77 | "MQTT_PORT": "int?", 78 | "MQTT_RETAIN": "bool?", 79 | "MQTT_DISCOVERY": "bool?", 80 | "MQTT_DISCOVERY_PREFIX": "str?", 81 | "MQTT_TLS_CACERTS": "str?", 82 | "MQTT_TLS_INSECURE": "bool?", 83 | "DEBUG_LEVEL": "str?", 84 | 85 | "USERS": [ 86 | { 87 | "NAME": "str", 88 | "SEX": "str", 89 | "GT": "int", 90 | "LT": "int", 91 | "HEIGHT": "int", 92 | "DOB": "str" 93 | } 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /mi-scale/apparmor.txt: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | profile xiaomi_mi_scale flags=(attach_disconnected,mediate_deleted) { 4 | #include 5 | 6 | # Capabilities 7 | network, 8 | capability, 9 | file, 10 | 11 | # ### 12 | # included from https://github.com/edgexfoundry-holding/device-bluetooth-c/blob/main/docker-ble-policy 13 | # ### 14 | dbus (send, receive) bus=system peer=(name=org.bluez, label=unconfined), 15 | dbus (send, receive) bus=system interface=org.freedesktop.DBus peer=(label=unconfined), 16 | 17 | # ### 18 | # included from https://github.com/jdstrand/snapd/blob/4befc00e3318a3231e96b38b575bf6e637ddad6c/interfaces/builtin/bluez.go 19 | # ### 20 | dbus (receive, send) 21 | bus=system 22 | interface=org.bluez.* 23 | peer=(label=unconfined), 24 | dbus (receive, send) 25 | bus=system 26 | path=/org/bluez{,/**} 27 | interface=org.freedesktop.DBus.* 28 | peer=(label=unconfined), 29 | dbus (receive, send) 30 | bus=system 31 | path=/ 32 | interface=org.freedesktop.DBus.* 33 | peer=(label=unconfined), 34 | 35 | 36 | # ### 37 | # included from https://developers.home-assistant.io/docs/add-ons/presentation#apparmor 38 | # ### 39 | signal (send) set=(kill,term,int,hup,cont), 40 | 41 | # Receive signals from S6-Overlay 42 | signal (send,receive) peer=*_xiaomi_mi_scale, 43 | 44 | # S6-Overlay 45 | /init ix, 46 | /bin/** ix, 47 | /usr/bin/** ix, 48 | /run/{s6,s6-rc*,service}/** ix, 49 | /package/** ix, 50 | /command/** ix, 51 | /etc/services.d/** rwix, 52 | /etc/cont-init.d/** rwix, 53 | /etc/cont-finish.d/** rwix, 54 | /run/{,**} rwk, 55 | /dev/tty rw, 56 | 57 | # Bashio 58 | /usr/lib/bashio/** ix, 59 | /tmp/** rwk, 60 | 61 | # Access to options.json and other files within your addon 62 | /data/** rw, 63 | 64 | # Access to mapped volumes specified in config.json 65 | /share/** rw, 66 | 67 | 68 | # ### 69 | # included from https://gist.github.com/disconnect3d/d578af68b09ab56db657854ec03879aa 70 | # (docker-default profile which would usually be used for this container) 71 | # ### 72 | signal (receive) peer=unconfined, 73 | signal (send,receive) peer=docker-default, 74 | 75 | deny @{PROC}/* w, # deny write for all files directly in /proc (not in a subdir) 76 | # deny write to files not in /proc//** or /proc/sys/** 77 | deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9]*}/** w, 78 | deny @{PROC}/sys/[^k]** w, # deny /proc/sys except /proc/sys/k* (effectively /proc/sys/kernel) 79 | deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w, # deny everything except shm* in /proc/sys/kernel/ 80 | deny @{PROC}/sysrq-trigger rwklx, 81 | deny @{PROC}/kcore rwklx, 82 | 83 | deny mount, 84 | 85 | deny /sys/[^f]*/** wklx, 86 | deny /sys/f[^s]*/** wklx, 87 | deny /sys/fs/[^c]*/** wklx, 88 | deny /sys/fs/c[^g]*/** wklx, 89 | deny /sys/fs/cg[^r]*/** wklx, 90 | deny /sys/firmware/** rwklx, 91 | deny /sys/kernel/security/** rwklx, 92 | 93 | 94 | # suppress ptrace denials when using 'docker ps' or using 'ps' inside a container 95 | ptrace (trace,read) peer=docker-default, 96 | } -------------------------------------------------------------------------------- /mi-scale/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.3.6] - 2022-10-10 2 | ### Added 3 | - Added apparmor Security to support Supervised Installation. (Fixes [67](https://github.com/lolouk44/hassio-addons/issues/67) - Thanks @MariusHerget) 4 | 5 | ## [0.3.5] - 2022-10-10 6 | ### Added 7 | - Added extra logging. Logging Level can be set from config file. 8 | - Deprecated options MISCALE_VERSION, TIME_INTERVAL. 9 | ### Changed 10 | - Restored HCI settings handling 11 | 12 | ## [0.3.4] - 2022-10-05 13 | ### Changed 14 | - Restored MQTT Discovery. ([fixes #65](https://github.com/lolouk44/hassio-addons/issues/65)) 15 | - Removed no longer needed MISCALE_VERSION 16 | 17 | ## [0.3.3] - 2022-10-03 18 | ### Changed 19 | - Restoring handling of V1 scales. ([fixes #64](https://github.com/lolouk44/hassio-addons/issues/64)) 20 | 21 | ## [0.3.2] - 2022-10-03 22 | ### Changed 23 | - Fixed missing dbus option. ([fixes #63](https://github.com/lolouk44/hassio-addons/issues/63)) 24 | 25 | ## [0.3.1] - 2022-10-02 26 | ### Changed 27 | - Fixed MQTT Config. ([fixes #55](https://github.com/lolouk44/xiaomi_mi_scale/issues/55)) 28 | 29 | ## [0.3.0] - 2022-10-02 30 | ### Changed 31 | - Stopped using deprecated/no longer supported bluepy library and replaced with bleak, requiring major code overhaul. ([fixes #59](https://github.com/lolouk44/hassio-addons/issues/59)) 32 | - Updated documentation to reflect MQTT integration (moved out of sensor config) 33 | ### Breaking Changes 34 | - If using a MiScale V1, make sure you add the MISCALE_VERSION to your options.json file (see doc) 35 | 36 | ## [0.2.8] - 2022-02-03 37 | ### Changed 38 | - Changed time format for datestamp to contain timezone ([fixes #59](https://github.com/lolouk44/hassio-addons/issues/59)) 39 | 40 | ## [0.2.7] - 2022-01-13 41 | ### Added 42 | - Added support for Long Term Statistics (HA 2021.9 minimum required) 43 | - Impedance posted to MQTT ([fixes #56](https://github.com/lolouk44/hassio-addons/issues/56)) 44 | 45 | ## [0.2.6] - 2021-06-28 46 | ### Changed 47 | - Fixed handling of MQTT_PORT and TIME_INTERVAL 48 | 49 | ## [0.2.4] - 2021-05-10 50 | ### Fixed 51 | - Fixed user lookup by non kg weight (https://github.com/lolouk44/hassio-addons/issues/36) 52 | - Added pre-relase workflow 53 | 54 | ## [0.2.1] - 2021-05-10 55 | ### Changed 56 | - Split README.md into README.md and DOCS.md, revisit and improve content ([PR-37](https://github.com/lolouk44/hassio-addons/pull/37)) 57 | 58 | ## [0.2.0] - 2021-02-11 59 | ### Breaking Changes 60 | - Allow any amount of users to be configured (removes limit of three) (based on [PR-18](https://github.com/lolouk44/hassio-addons/pull/18)) 61 | - Users need to be added as a list (see config example) 62 | - All users must now provide a GT and LT value 63 | - Try not to overlap ranges - if you do it will ony pick the first one that matches 64 | 65 | ## [0.1.18] - 2021-02-11 66 | ### Changed 67 | - Restored correct code after accepting a PR based on older code in 0.1.17 68 | 69 | ## [0.1.17] - 2021-02-10 70 | ### Changed 71 | - Reduced docker image size 72 | - Added BLUEPY_PASSIVE_SCAN Option to help with some Raspberry Pi devices getting errors like `Bluetooth connection error: Failed to execute management command ‘le on’` 73 | - Fixed config.json startup deprecated option (fixes https://github.com/lolouk44/hassio-addons/issues/32) 74 | 75 | ## [0.1.16] - 2020-11-26 76 | ### Changed 77 | - Fixed MQTT Discovery Message 78 | 79 | ## [0.1.15] - 2020-11-26 80 | ### Changed 81 | - Fixed MQTT Discovery Message 82 | 83 | ## [0.1.14] - 2020-11-24 84 | ### Changed 85 | - 2nd attempt to fix executable files (fixes https://github.com/lolouk44/hassio-addons/issues/23) 86 | - Fixed image links in README file 87 | 88 | ## [0.1.13] - 2020-11-24 89 | ### Changed 90 | - Fixed executable files ~~(fixes https://github.com/lolouk44/hassio-addons/issues/23)~~ 91 | 92 | ## [0.1.12] - 2020-11-23 93 | ### Changed 94 | - Fixed workflow for automatic docker images building 95 | 96 | ## [0.1.11] - 2020-11-23 97 | ### Changed 98 | - Remove additional logging for Scale V1 that was used for testing 99 | - Changed timestamp to default python format (fixes https://github.com/lolouk44/xiaomi_mi_scale/issues/29) 100 | - Removed hard-coded 'unit_f_measurement' in the MQTT Discovery (fixes https://github.com/lolouk44/hassio-addons/issues/22) 101 | - Fixed hard coded MQTT Discovery Prefix (fixes https://github.com/lolouk44/xiaomi_mi_scale/issues/35) 102 | - Change measures format to be numbers instead of string where applicable (https://github.com/lolouk44/xiaomi_mi_scale/pull/36) 103 | ### Added 104 | - Created workflow to automatically build docker images on new releases (Thanks @AiiR42 for your help) 105 | 106 | 107 | ## [0.1.10] - 2020-09-09 108 | ### Changed 109 | - Fixed issue with detection of boolean in MQTT_DISCOVERY (https://github.com/lolouk44/hassio-addons/issues/16 and https://github.com/lolouk44/xiaomi_mi_scale/issues/31) 110 | 111 | ## [0.1.9] - 2020-09-08 112 | ### Changed 113 | - Fixed typo in MQTT message following the **breaking change** to snake_case attributes in 0.1.8 114 | 115 | ## [0.1.8] - 2020-09-08 116 | ### Breaking Changes 117 | - Attributes are now snake_case (fixes https://github.com/lolouk44/xiaomi_mi_scale/issues/24) 118 | ### Changed 119 | - Fixed default MQTT Prefix in config.json typo (fixes https://github.com/lolouk44/hassio-addons/issues/6) 120 | - Fixed MQTT Discovery value check to discover 121 | - Changed timestamp to default python format 122 | - Changes the bluetooth reset from reset to down-wait-up (fixes https://github.com/lolouk44/hassio-addons/issues/13) 123 | - Fixed hard coded hci0 to provided hci interface when performing a reset 124 | - Fixed weight in Lbs not detected on Scale V1 (XMTZCO1HM) (fixes https://github.com/lolouk44/xiaomi_mi_scale/issues/28) 125 | - Fixed body calculations for non kg weights 126 | - Updated README 127 | ### Added 128 | - Added unit to attributes 129 | 130 | ## [0.1.7] - 2020-07-06 131 | ### Added 132 | - repository.json to make it a real add-on repo (fixes https://github.com/lolouk44/hassio-addons/issues/4) 133 | ## Changed 134 | - Now truly handles optional config entries(fixes https://github.com/lolouk44/hassio-addons/issues/3) 135 | - MQTT Discovery set wtih retain flag (fixes https://github.com/lolouk44/hassio-addons/issues/2) 136 | - README updated to use Xiaomi Mi Fit App to retrieve the MAC Address (fixes https://github.com/lolouk44/xiaomi_mi_scale/pull/25) 137 | 138 | ## [0.1.6] - 2020-07-01 139 | ### Added 140 | - Docker Image so install is quicker (no local build required). 141 | 142 | ## [0.1.5] - 2020-07-01 143 | ### Added 144 | - MQTT Discovery Support. 145 | 146 | ## [0.1.4] - 2020-06-29 147 | ### Added 148 | - First release (version in line with non Add-On script for ease of maintenance). 149 | -------------------------------------------------------------------------------- /mi-scale/DOCS.md: -------------------------------------------------------------------------------- 1 | # Supported Scales 2 | 3 | The following scales are supported by the Add-on: 4 | Name | Model | Picture 5 | --- | --- | --- 6 | [Mi Smart Scale 2](https://www.mi.com/global/scale)                                                                                               | XMTZC04HM | Mi Scale_2 7 | [Mi Body Composition Scale](https://www.mi.com/global/mi-body-composition-scale/) | XMTZC02HM | Mi Scale 8 | [Mi Body Composition Scale 2](https://c.mi.com/thread-2289389-1-0.html) | XMTZC05HM | Mi Body Composition Scale 2 9 | 10 | # Setup 11 | 1. Retrieve the scale's MAC address from the [Xiaomi Mi Fit app](https://play.google.com/store/apps/details?id=com.xiaomi.hm.health&hl=en&gl=US), or alternatively, you can also use the `bluetoothctl` tool on Linux and type `scan on` to scan for BLE devices (`sudo hcitool lescan` might also work). The device should be named `MIBFS` or similar.
12 | Showing the MAC address in the Xiaomi Mi Fit app 13 | 14 | 15 | 1. Open Home Assistant and navigate to the "Add-on Store". Click on the 3 dots (top right) and select "Repositories". 16 | 2. Enter `https://github.com/lolouk44/hassio-addons` in the box and click on "Add". 17 | 3. You should now see "Lolouk44 Add-Ons" at the bottom list. 18 | 4. Click on "Xiaomi Mi Scale", then click "Install". 19 | 5. Under the "Configuration" tab, change the settings appropriately (at least MQTT parameters, user properties, and MAC address), see [Parameters](#parameters). 20 | 6. Start the Add-on. 21 | 22 | ## Important: 23 | If using the Add-On outside of Home Assistant Operating System / through a docker container, make sure the dbus is shared with the container running Home Assistant. This is typically done by adding the following line in your docker run command: 24 | `-v /var/run/dbus/:/var/run/dbus/:ro` 25 | or the following lines in your docker-compose file: 26 | ``` 27 | volumes: 28 | - /var/run/dbus/:/var/run/dbus/:ro 29 | ``` 30 | 31 | ## Parameters 32 | Option | Type | Required | Description 33 | --- | --- | --- | --- 34 | HCI_DEV | string | No | Bluetooth hci device to use. Defaults to `hci0` 35 | BLUEPY_PASSIVE_SCAN | bool | No | Try to set to true if getting an error like `Bluetooth connection error: Failed to execute management command ‘le on’` on a Raspberry Pi. Defaults to `false` 36 | MISCALE_MAC | string | Yes | MAC address of your scale 37 | MQTT_PREFIX | string | No | MQTT topic prefix, defaults to `miscale` 38 | MQTT_HOST | string | Yes | MQTT server, defaults to `127.0.0.1` 39 | MQTT_USERNAME | string | No | Username for MQTT server (comment out if not required) 40 | MQTT_PASSWORD | string | No | Password for MQTT (comment out if not required) 41 | MQTT_PORT | int | No | Port of your MQTT server, defaults to 1883 42 | MQTT_DISCOVERY | bool | No | Whether you want MQTT discovery for Home Assistant, defaults to `true` 43 | MQTT_DISCOVERY_PREFIX | string | No | MQTT discovery prefix for Home Assistant, defaults to `homeassistant` 44 | DEBUG_LEVEL | string | No | Logging level. Possible values: 'CRITICAL', 'ERROR', 'WARNING', 'INFO', 'DEBUG'. Defaults to 'INFO' 45 | USERS | List | Yes | List of users to add, see below 46 | 47 | 48 | Auto-gender selection/config: this is used to create the calculations such as BMI, Water/Bone Mass, etc. 49 | Here is the logic used to assign a measured weight to a user: 50 | - If the weight is within the range of a user's defined values for GT and LT, then it will be assigned (published) to that user. 51 | - If the weight matches two separate user ranges, it will only be assigned to the first user that matched. So don't create overlapping ranges! 52 | 53 | User Option | Type | Required | Description 54 | --- | --- | --- | --- 55 | GT | int | Yes | Greater Than - Weight must be greater than this value; this will be the lower limit for the weight range of the user 56 | LT | int | Yes | Less Than - Weight must be less than this value; this will be the upper limit for the weight range of the user 57 | SEX | string | Yes | Gender of the user (male / female) 58 | NAME | string | Yes | Name of the user 59 | HEIGHT | int | Yes | Height (in cm) of the user 60 | DOB | string | Yes | Date of Birth of the user (in yyyy-mm-dd format) 61 | 62 | Note: the weight definitions must be in the same unit as the scale (kg, Lbs, or jin). 63 | 64 | 65 | # Home Assistant Setup 66 | In the `mqtt:` block, enter as many blocks as users configured in your environment variables. 67 | If you already have an `mqtt:` and/or `sensor:` block, do not create another one but simply add the "missing" bits under the relevant block header. 68 | Note: Only weight entities are automatically added via the MQTT discovery. 69 | 70 | 71 | ```yaml 72 | mqtt: 73 | sensor: 74 | - name: "Example Name Weight" 75 | state_topic: "miscale/USER_NAME/weight" 76 | value_template: "{{ value_json['weight'] }}" 77 | unit_of_measurement: "kg" 78 | json_attributes_topic: "miscale/USER_NAME/weight" 79 | icon: mdi:scale-bathroom 80 | # Below lines only needed if long term statistics are required 81 | state_class: "measurement" 82 | 83 | - name: "Example Name BMI" 84 | state_topic: "miscale/USER_NAME/weight" 85 | value_template: "{{ value_json['bmi'] }}" 86 | icon: mdi:human-pregnant 87 | unit_of_measurement: "kg/m2" 88 | # Below lines only needed if long term statistics are required 89 | state_class: "measurement" 90 | ``` 91 | 92 | Example of the Lovelace card in HA 🠲 Example of the details of the Lovelace card in HA 93 | 94 | 95 | 96 | # Acknowledgements 97 | 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), [@fabir-git](https://github.com/fabir-git), [@snozzley](https://github.com/snozzley), [CodeFinder2](https://github.com/CodeFinder2) [@MariusHerget](https://github.com/MariusHerget) for their PRs and collaboration. 98 | -------------------------------------------------------------------------------- /mi-scale/src/body_scales.py: -------------------------------------------------------------------------------- 1 | class bodyScales: 2 | def __init__(self, age, height, sex, weight, scaleType='xiaomi'): 3 | self.age = age 4 | self.height = height 5 | self.sex = sex 6 | self.weight = weight 7 | 8 | if scaleType == 'xiaomi': 9 | self.scaleType = 'xiaomi' 10 | else: 11 | self.scaleType = 'holtek' 12 | 13 | # Get BMI scale 14 | def getBMIScale(self): 15 | if self.scaleType == 'xiaomi': 16 | # Amazfit/new mi fit 17 | #return [18.5, 24, 28] 18 | # Old mi fit // amazfit for body figure 19 | return [18.5, 25.0, 28.0, 32.0] 20 | elif self.scaleType == 'holtek': 21 | return [18.5, 25.0, 30.0] 22 | 23 | # Get fat percentage scale 24 | def getFatPercentageScale(self): 25 | # The included tables where quite strange, maybe bogus, replaced them with better ones... 26 | if self.scaleType == 'xiaomi': 27 | scales = [ 28 | {'min': 0, 'max': 12, 'female': [12.0, 21.0, 30.0, 34.0], 'male': [7.0, 16.0, 25.0, 30.0]}, 29 | {'min': 12, 'max': 14, 'female': [15.0, 24.0, 33.0, 37.0], 'male': [7.0, 16.0, 25.0, 30.0]}, 30 | {'min': 14, 'max': 16, 'female': [18.0, 27.0, 36.0, 40.0], 'male': [7.0, 16.0, 25.0, 30.0]}, 31 | {'min': 16, 'max': 18, 'female': [20.0, 28.0, 37.0, 41.0], 'male': [7.0, 16.0, 25.0, 30.0]}, 32 | {'min': 18, 'max': 40, 'female': [21.0, 28.0, 35.0, 40.0], 'male': [11.0, 17.0, 22.0, 27.0]}, 33 | {'min': 40, 'max': 60, 'female': [22.0, 29.0, 36.0, 41.0], 'male': [12.0, 18.0, 23.0, 28.0]}, 34 | {'min': 60, 'max': 100, 'female': [23.0, 30.0, 37.0, 42.0], 'male': [14.0, 20.0, 25.0, 30.0]}, 35 | ] 36 | 37 | elif self.scaleType == 'holtek': 38 | scales = [ 39 | {'min': 0, 'max': 21, 'female': [18, 23, 30, 35], 'male': [8, 14, 21, 25]}, 40 | {'min': 21, 'max': 26, 'female': [19, 24, 30, 35], 'male': [10, 15, 22, 26]}, 41 | {'min': 26, 'max': 31, 'female': [20, 25, 31, 36], 'male': [11, 16, 21, 27]}, 42 | {'min': 31, 'max': 36, 'female': [21, 26, 33, 36], 'male': [13, 17, 25, 28]}, 43 | {'min': 36, 'max': 41, 'female': [22, 27, 34, 37], 'male': [15, 20, 26, 29]}, 44 | {'min': 41, 'max': 46, 'female': [23, 28, 35, 38], 'male': [16, 22, 27, 30]}, 45 | {'min': 46, 'max': 51, 'female': [24, 30, 36, 38], 'male': [17, 23, 29, 31]}, 46 | {'min': 51, 'max': 56, 'female': [26, 31, 36, 39], 'male': [19, 25, 30, 33]}, 47 | {'min': 56, 'max': 100, 'female': [27, 32, 37, 40], 'male': [21, 26, 31, 34]}, 48 | ] 49 | 50 | for scale in scales: 51 | if self.age >= scale['min'] and self.age < scale['max']: 52 | return scale[self.sex] 53 | 54 | # Get muscle mass scale 55 | def getMuscleMassScale(self): 56 | if self.scaleType == 'xiaomi': 57 | scales = [ 58 | {'min': {'male': 170, 'female': 160}, 'female': [36.5, 42.6], 'male': [49.4, 59.5]}, 59 | {'min': {'male': 160, 'female': 150}, 'female': [32.9, 37.6], 'male': [44.0, 52.5]}, 60 | {'min': {'male': 0, 'female': 0}, 'female': [29.1, 34.8], 'male': [38.5, 46.6]}, 61 | ] 62 | elif self.scaleType == 'holtek': 63 | scales = [ 64 | {'min': {'male': 170, 'female': 170}, 'female': [36.5, 42.5], 'male': [49.5, 59.4]}, 65 | {'min': {'male': 160, 'female': 160}, 'female': [32.9, 37.5], 'male': [44.0, 52.4]}, 66 | {'min': {'male': 0, 'female': 0}, 'female': [29.1, 34.7], 'male': [38.5, 46.5]} 67 | ] 68 | 69 | for scale in scales: 70 | if self.height >= scale['min'][self.sex]: 71 | return scale[self.sex] 72 | 73 | 74 | 75 | # Get water percentage scale 76 | def getWaterPercentageScale(self): 77 | if self.scaleType == 'xiaomi': 78 | if self.sex == 'male': 79 | return [55.0, 65.1] 80 | elif self.sex == 'female': 81 | return [45.0, 60.1] 82 | elif self.scaleType == 'holtek': 83 | return [53, 67] 84 | 85 | 86 | # Get visceral fat scale 87 | def getVisceralFatScale(self): 88 | # Actually the same in mi fit/amazfit and holtek's sdk 89 | return [10.0, 15.0] 90 | 91 | 92 | # Get bone mass scale 93 | def getBoneMassScale(self): 94 | if self.scaleType == 'xiaomi': 95 | scales = [ 96 | {'male': {'min': 75.0, 'scale': [2.0, 4.2]}, 'female': {'min': 60.0, 'scale': [1.8, 3.9]}}, 97 | {'male': {'min': 60.0, 'scale': [1.9, 4.1]}, 'female': {'min': 45.0, 'scale': [1.5, 3.8]}}, 98 | {'male': {'min': 0.0, 'scale': [1.6, 3.9]}, 'female': {'min': 0.0, 'scale': [1.3, 3.6]}}, 99 | ] 100 | 101 | for scale in scales: 102 | if self.weight >= scale[self.sex]['min']: 103 | return scale[self.sex]['scale'] 104 | 105 | elif self.scaleType == 'holtek': 106 | scales = [ 107 | {'female': {'min': 60, 'optimal': 2.5}, 'male': {'min': 75, 'optimal': 3.2}}, 108 | {'female': {'min': 45, 'optimal': 2.2}, 'male': {'min': 69, 'optimal': 2.9}}, 109 | {'female': {'min': 0, 'optimal': 1.8}, 'male': {'min': 0, 'optimal': 2.5}} 110 | ] 111 | 112 | for scale in scales: 113 | if self.weight >= scale[self.sex]['min']: 114 | return [scale[self.sex]['optimal']-1, scale[self.sex]['optimal']+1] 115 | 116 | 117 | # Get BMR scale 118 | def getBMRScale(self): 119 | if self.scaleType == 'xiaomi': 120 | coefficients = { 121 | 'male': {30: 21.6, 50: 20.07, 100: 19.35}, 122 | 'female': {30: 21.24, 50: 19.53, 100: 18.63} 123 | } 124 | elif self.scaleType == 'holtek': 125 | coefficients = { 126 | 'female': {12: 34, 15: 29, 17: 24, 29: 22, 50: 20, 120: 19}, 127 | 'male': {12: 36, 15: 30, 17: 26, 29: 23, 50: 21, 120: 20} 128 | } 129 | 130 | for age, coefficient in coefficients[self.sex].items(): 131 | if self.age < age: 132 | return [self.weight * coefficient] 133 | 134 | 135 | # Get protein scale (hardcoded in mi fit) 136 | def getProteinPercentageScale(self): 137 | # Actually the same in mi fit and holtek's sdk 138 | return [16, 20] 139 | 140 | # Get ideal weight scale (BMI scale converted to weights) 141 | def getIdealWeightScale(self): 142 | scale = [] 143 | for bmiScale in self.getBMIScale(): 144 | scale.append((bmiScale*self.height)*self.height/10000) 145 | return scale 146 | 147 | # Get Body Score scale 148 | def getBodyScoreScale(self): 149 | # very bad, bad, normal, good, better 150 | return [50.0, 60.0, 80.0, 90.0] 151 | 152 | # Return body type scale 153 | def getBodyTypeScale(self): 154 | return ['obese', 'overweight', 'thick-set', 'lack-exerscise', 'balanced', 'balanced-muscular', 'skinny', 'balanced-skinny', 'skinny-muscular'] 155 | 156 | -------------------------------------------------------------------------------- /mi-scale/src/body_score.py: -------------------------------------------------------------------------------- 1 | 2 | # Reverse engineered from amazfit's app (also known as Mi Fit) 3 | from body_scales import bodyScales 4 | class bodyScore: 5 | 6 | def __init__(self, age, sex, height, weight, bmi, bodyfat, muscle, water, visceral_fat, bone, basal_metabolism, protein): 7 | self.age = age 8 | self.sex = sex 9 | self.height = height 10 | self.weight = weight 11 | self.bmi = bmi 12 | self.bodyfat = bodyfat 13 | self.muscle = muscle 14 | self.water = water 15 | self.visceral_fat = visceral_fat 16 | self.bone = bone 17 | self.basal_metabolism = basal_metabolism 18 | self.protein = protein 19 | self.scales = bodyScales(age, height, sex, weight) 20 | 21 | def getBodyScore(self): 22 | score = 100 23 | score -= self.getBmiDeductScore() 24 | score -= self.getBodyFatDeductScore() 25 | score -= self.getMuscleDeductScore() 26 | score -= self.getWaterDeductScore() 27 | score -= self.getVisceralFatDeductScore() 28 | score -= self.getBoneDeductScore() 29 | score -= self.getBasalMetabolismDeductScore() 30 | if self.protein: 31 | score -= self.getProteinDeductScore() 32 | return score 33 | 34 | def getMalus(self, data, min_data, max_data, max_malus, min_malus): 35 | result = ((data - max_data) / (min_data - max_data)) * float(max_malus - min_malus) 36 | if result >= 0.0: 37 | return result 38 | return 0.0 39 | 40 | def getBmiDeductScore(self): 41 | if not self.height >= 90: 42 | # "BMI is not reasonable 43 | return 0.0 44 | 45 | bmi_low = 15.0 46 | bmi_verylow = 14.0 47 | bmi_normal = 18.5 48 | bmi_overweight = 28.0 49 | bmi_obese = 32.0 50 | fat_scale = self.scales.getFatPercentageScale() 51 | 52 | # Perfect range (bmi >= 18.5 and bodyfat not high for adults, bmi >= 15.0 for kids 53 | if self.bmi >= 18.5 and self.age >= 18 and self.bodyfat < fat_scale[2]: 54 | return 0.0 55 | elif self.bmi >= bmi_verylow and self.age < 18 and self.bodyfat < fat_scale[2]: 56 | return 0.0 57 | 58 | # Extremely skinny (bmi < 14) 59 | elif self.bmi <= bmi_verylow: 60 | return 30.0 61 | # Too skinny (bmi between 14 and 15) 62 | elif self.bmi > bmi_verylow and self.bmi < bmi_low: 63 | return self.getMalus(self.bmi, bmi_verylow, bmi_low, 30, 15) + 15.0 64 | # Skinny (for adults, between 15 and 18.5) 65 | elif self.bmi >= bmi_low and self.bmi < bmi_normal and self.age >= 18: 66 | return self.getMalus(self.bmi, 15.0, 18.5, 15, 5) + 5.0 67 | 68 | # Normal or high bmi but too much bodyfat 69 | elif ((self.bmi >= bmi_low and self.age < 18) or (self.bmi >= bmi_normal and self.age >= 18)) and self.bodyfat >= fat_scale[2]: 70 | # Obese 71 | if self.bmi >= bmi_obese: 72 | return 10.0 73 | # Overweight 74 | if self.bmi > bmi_overweight: 75 | return self.getMalus(self.bmi, 28.0, 25.0, 5, 10) + 5.0 76 | else: 77 | return 0.0 78 | 79 | def getBodyFatDeductScore(self): 80 | scale = self.scales.getFatPercentageScale() 81 | 82 | if self.sex == 'male': 83 | best = scale[2] - 3.0 84 | elif self.sex == 'female': 85 | best = scale[2] - 2.0 86 | 87 | # Slighly low in fat or low part or normal fat 88 | if self.bodyfat >= scale[0] and self.bodyfat < best: 89 | return 0.0 90 | elif self.bodyfat >= scale[3]: 91 | return 20.0 92 | else: 93 | # Sightly high body fat 94 | if self.bodyfat < scale[3]: 95 | return self.getMalus(self.bodyfat, scale[3], scale[2], 20, 10) + 10.0 96 | 97 | # High part of normal fat 98 | elif self.bodyfat <= scale[2]: 99 | return self.getMalus(self.bodyfat, scale[2], best, 3, 9) + 3.0 100 | 101 | # Very low in fat 102 | elif self.bodyfat < scale[0]: 103 | return self.getMalus(self.bodyfat, 1.0, scale[0], 3, 10) + 3.0 104 | 105 | 106 | def getMuscleDeductScore(self): 107 | scale = self.scales.getMuscleMassScale() 108 | 109 | # For some reason, there's code to return self.calculate(muscle, normal[0], normal[0]+2.0, 3, 5) + 3.0 110 | # if your muscle is between normal[0] and normal[0] + 2.0, but it's overwritten with 0.0 before return 111 | if self.muscle >= scale[0]: 112 | return 0.0 113 | elif self.muscle < (scale[0] - 5.0): 114 | return 10.0 115 | else: 116 | return self.getMalus(self.muscle, scale[0] - 5.0, scale[0], 10, 5) + 5.0 117 | 118 | # No malus = normal or good; maximum malus (10.0) = less than normal-5.0; 119 | # malus = between 5 and 10, on your water being between normal-5.0 and normal 120 | def getWaterDeductScore(self): 121 | scale = self.scales.getWaterPercentageScale() 122 | 123 | if self.water >= scale[0]: 124 | return 0.0 125 | elif self.water <= (scale[0] - 5.0): 126 | return 10.0 127 | else: 128 | return self.getMalus(self.water, scale[0] - 5.0, scale[0], 10, 5) + 5.0 129 | 130 | # No malus = normal; maximum malus (15.0) = very high; malus = between 10 and 15 131 | # with your visceral fat in your high range 132 | def getVisceralFatDeductScore(self): 133 | scale = self.scales.getVisceralFatScale() 134 | 135 | if self.visceral_fat < scale[0]: 136 | # For some reason, the original app would try to 137 | # return 3.0 if vfat == 8 and 5.0 if vfat == 9 138 | # but i's overwritten with 0.0 anyway before return 139 | return 0.0 140 | elif self.visceral_fat >= scale[1]: 141 | return 15.0 142 | else: 143 | return self.getMalus(self.visceral_fat, scale[1], scale[0], 15, 10) + 10.0 144 | 145 | def getBoneDeductScore(self): 146 | scale = self.scales.getBoneMassScale() 147 | 148 | if self.bone >= scale[0]: 149 | return 0.0 150 | elif self.bone <= (scale[0] - 0.3): 151 | return 10.0 152 | else: 153 | return self.getMalus(self.bone, scale[0] - 0.3, scale[0], 10, 5) + 5.0 154 | 155 | def getBasalMetabolismDeductScore(self): 156 | # Get normal BMR 157 | normal = self.scales.getBMRScale()[0] 158 | 159 | if self.basal_metabolism >= normal: 160 | return 0.0 161 | elif self.basal_metabolism <= (normal - 300): 162 | return 6.0 163 | else: 164 | # It's really + 5.0 in the app, but it's probably a mistake, should be 3.0 165 | return self.getMalus(self.basal_metabolism, normal - 300, normal, 6, 3) + 5.0 166 | 167 | 168 | # Get protein percentage malus 169 | def getProteinDeductScore(self): 170 | # low: 10,16; normal: 16,17 171 | # Check limits 172 | if self.protein > 17.0: 173 | return 0.0 174 | elif self.protein < 10.0: 175 | return 10.0 176 | else: 177 | # Return values for low proteins or normal proteins 178 | if self.protein <= 16.0: 179 | return self.getMalus(self.protein, 10.0, 16.0, 10, 5) + 5.0 180 | elif self.protein <= 17.0: 181 | return self.getMalus(self.protein, 16.0, 17.0, 5, 3) + 3.0 182 | -------------------------------------------------------------------------------- /mi-scale/src/Xiaomi_Scale_Body_Metrics.py: -------------------------------------------------------------------------------- 1 | from math import floor 2 | import sys 3 | from body_scales import bodyScales 4 | 5 | class bodyMetrics: 6 | def __init__(self, weight, height, age, sex, impedance): 7 | self.weight = weight 8 | self.height = height 9 | self.age = age 10 | self.sex = sex 11 | self.impedance = impedance 12 | self.scales = bodyScales(age, height, sex, weight) 13 | 14 | # Check for potential out of boundaries 15 | if self.height > 220: 16 | print("Height is too high (limit: >220cm) or scale is sleeping") 17 | sys.stderr.write('Height is over 220cm\n') 18 | exit() 19 | elif weight < 10 or weight > 200: 20 | print("Weight is either too low or too high (limits: <10kg and >200kg)") 21 | sys.stderr.write('Weight is below 10kg or above 200kg\n') 22 | exit() 23 | elif age > 99: 24 | print("Age is too high (limit >99 years)") 25 | sys.stderr.write('Age is above 99 years\n') 26 | exit() 27 | elif impedance > 3000: 28 | print("Impedance is above 3000 Ohm") 29 | sys.stderr.write('Impedance is above 3000 Ohm\n') 30 | exit() 31 | 32 | # Set the value to a boundary if it overflows 33 | def checkValueOverflow(self, value, minimum, maximum): 34 | if value < minimum: 35 | return minimum 36 | elif value > maximum: 37 | return maximum 38 | else: 39 | return value 40 | 41 | # Get LBM coefficient (with impedance) 42 | def getLBMCoefficient(self): 43 | lbm = (self.height * 9.058 / 100) * (self.height / 100) 44 | lbm += self.weight * 0.32 + 12.226 45 | lbm -= self.impedance * 0.0068 46 | lbm -= self.age * 0.0542 47 | return lbm 48 | 49 | # Get BMR 50 | def getBMR(self): 51 | if self.sex == 'female': 52 | bmr = 864.6 + self.weight * 10.2036 53 | bmr -= self.height * 0.39336 54 | bmr -= self.age * 6.204 55 | else: 56 | bmr = 877.8 + self.weight * 14.916 57 | bmr -= self.height * 0.726 58 | bmr -= self.age * 8.976 59 | 60 | # Capping 61 | if self.sex == 'female' and bmr > 2996: 62 | bmr = 5000 63 | elif self.sex == 'male' and bmr > 2322: 64 | bmr = 5000 65 | return self.checkValueOverflow(bmr, 500, 10000) 66 | 67 | # Get fat percentage 68 | def getFatPercentage(self): 69 | # Set a constant to remove from LBM 70 | if self.sex == 'female' and self.age <= 49: 71 | const = 9.25 72 | elif self.sex == 'female' and self.age > 49: 73 | const = 7.25 74 | else: 75 | const = 0.8 76 | 77 | # Calculate body fat percentage 78 | LBM = self.getLBMCoefficient() 79 | 80 | if self.sex == 'male' and self.weight < 61: 81 | coefficient = 0.98 82 | elif self.sex == 'female' and self.weight > 60: 83 | coefficient = 0.96 84 | if self.height > 160: 85 | coefficient *= 1.03 86 | elif self.sex == 'female' and self.weight < 50: 87 | coefficient = 1.02 88 | if self.height > 160: 89 | coefficient *= 1.03 90 | else: 91 | coefficient = 1.0 92 | fatPercentage = (1.0 - (((LBM - const) * coefficient) / self.weight)) * 100 93 | 94 | # Capping body fat percentage 95 | if fatPercentage > 63: 96 | fatPercentage = 75 97 | return self.checkValueOverflow(fatPercentage, 5, 75) 98 | 99 | # Get water percentage 100 | def getWaterPercentage(self): 101 | waterPercentage = (100 - self.getFatPercentage()) * 0.7 102 | 103 | if (waterPercentage <= 50): 104 | coefficient = 1.02 105 | else: 106 | coefficient = 0.98 107 | 108 | # Capping water percentage 109 | if waterPercentage * coefficient >= 65: 110 | waterPercentage = 75 111 | return self.checkValueOverflow(waterPercentage * coefficient, 35, 75) 112 | 113 | # Get bone mass 114 | def getBoneMass(self): 115 | if self.sex == 'female': 116 | base = 0.245691014 117 | else: 118 | base = 0.18016894 119 | 120 | boneMass = (base - (self.getLBMCoefficient() * 0.05158)) * -1 121 | 122 | if boneMass > 2.2: 123 | boneMass += 0.1 124 | else: 125 | boneMass -= 0.1 126 | 127 | # Capping boneMass 128 | if self.sex == 'female' and boneMass > 5.1: 129 | boneMass = 8 130 | elif self.sex == 'male' and boneMass > 5.2: 131 | boneMass = 8 132 | return self.checkValueOverflow(boneMass, 0.5 , 8) 133 | 134 | # Get muscle mass 135 | def getMuscleMass(self): 136 | muscleMass = self.weight - ((self.getFatPercentage() * 0.01) * self.weight) - self.getBoneMass() 137 | 138 | # Capping muscle mass 139 | if self.sex == 'female' and muscleMass >= 84: 140 | muscleMass = 120 141 | elif self.sex == 'male' and muscleMass >= 93.5: 142 | muscleMass = 120 143 | 144 | return self.checkValueOverflow(muscleMass, 10 ,120) 145 | 146 | # Get Visceral Fat 147 | def getVisceralFat(self): 148 | if self.sex == 'female': 149 | if self.weight > (13 - (self.height * 0.5)) * -1: 150 | subsubcalc = ((self.height * 1.45) + (self.height * 0.1158) * self.height) - 120 151 | subcalc = self.weight * 500 / subsubcalc 152 | vfal = (subcalc - 6) + (self.age * 0.07) 153 | else: 154 | subcalc = 0.691 + (self.height * -0.0024) + (self.height * -0.0024) 155 | vfal = (((self.height * 0.027) - (subcalc * self.weight)) * -1) + (self.age * 0.07) - self.age 156 | else: 157 | if self.height < self.weight * 1.6: 158 | subcalc = ((self.height * 0.4) - (self.height * (self.height * 0.0826))) * -1 159 | vfal = ((self.weight * 305) / (subcalc + 48)) - 2.9 + (self.age * 0.15) 160 | else: 161 | subcalc = 0.765 + self.height * -0.0015 162 | vfal = (((self.height * 0.143) - (self.weight * subcalc)) * -1) + (self.age * 0.15) - 5.0 163 | 164 | return self.checkValueOverflow(vfal, 1 ,50) 165 | 166 | # Get BMI 167 | def getBMI(self): 168 | return self.checkValueOverflow(self.weight/((self.height/100)*(self.height/100)), 10, 90) 169 | 170 | # Get ideal weight (just doing a reverse BMI, should be something better) 171 | def getIdealWeight(self, orig=True): 172 | # Uses mi fit algorithm (or holtek's one) 173 | if orig and self.sex == 'female': 174 | return (self.height - 70) * 0.6 175 | elif orig and self.sex == 'male': 176 | return (self.height - 80) * 0.7 177 | else: 178 | return self.checkValueOverflow((22*self.height)*self.height/10000, 5.5, 198) 179 | 180 | # Get fat mass to ideal (guessing mi fit formula) 181 | def getFatMassToIdeal(self): 182 | mass = (self.weight * (self.getFatPercentage() / 100)) - (self.weight * (self.scales.getFatPercentageScale()[2] / 100)) 183 | if mass < 0: 184 | return {'type': 'to_gain', 'mass': mass*-1} 185 | else: 186 | return {'type': 'to_lose', 'mass': mass} 187 | 188 | # Get protetin percentage (warn: guessed formula) 189 | def getProteinPercentage(self, orig=True): 190 | # Use original algorithm from mi fit (or legacy guess one) 191 | if orig: 192 | proteinPercentage = (self.getMuscleMass() / self.weight) * 100 193 | proteinPercentage -= self.getWaterPercentage() 194 | else: 195 | proteinPercentage = 100 - (floor(self.getFatPercentage() * 100) / 100) 196 | proteinPercentage -= floor(self.getWaterPercentage() * 100) / 100 197 | proteinPercentage -= floor((self.getBoneMass()/self.weight*100) * 100) / 100 198 | 199 | return self.checkValueOverflow(proteinPercentage, 5, 32) 200 | 201 | # Get body type (out of nine possible) 202 | def getBodyType(self): 203 | if self.getFatPercentage() > self.scales.getFatPercentageScale()[2]: 204 | factor = 0 205 | elif self.getFatPercentage() < self.scales.getFatPercentageScale()[1]: 206 | factor = 2 207 | else: 208 | factor = 1 209 | 210 | if self.getMuscleMass() > self.scales.getMuscleMassScale()[1]: 211 | return 2 + (factor * 3) 212 | elif self.getMuscleMass() < self.scales.getMuscleMassScale()[0]: 213 | return (factor * 3) 214 | else: 215 | return 1 + (factor * 3) 216 | 217 | # Get Metabolic Age 218 | def getMetabolicAge(self): 219 | if self.sex == 'female': 220 | metabolicAge = (self.height * -1.1165) + (self.weight * 1.5784) + (self.age * 0.4615) + (self.impedance * 0.0415) + 83.2548 221 | else: 222 | metabolicAge = (self.height * -0.7471) + (self.weight * 0.9161) + (self.age * 0.4184) + (self.impedance * 0.0517) + 54.2267 223 | return self.checkValueOverflow(metabolicAge, 15, 80) 224 | -------------------------------------------------------------------------------- /mi-scale/src/Xiaomi_Scale.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import asyncio 4 | import binascii 5 | from bleak import BleakScanner 6 | from collections import namedtuple 7 | from datetime import datetime 8 | import functools 9 | import json 10 | import paho.mqtt.publish as publish 11 | import subprocess 12 | import sys 13 | import logging 14 | import os 15 | 16 | import Xiaomi_Scale_Body_Metrics 17 | 18 | DEFAULT_DEBUG_LEVEL = "INFO" 19 | VERSION = "0.3.6" 20 | 21 | 22 | 23 | # User Config 24 | class USER: 25 | def __init__(self, name, gt, lt, sex, height, dob): 26 | self.NAME, self.GT, self.LT, self.SEX, self.HEIGHT, self.DOB 27 | 28 | 29 | def customUserDecoder(userDict): 30 | return namedtuple('USER', userDict.keys())(*userDict.values()) 31 | 32 | def MQTT_discovery(): 33 | """Published MQTT Discovery information if enabled in options.json""" 34 | for MQTTUser in (USERS): 35 | message = '{"name": "' + MQTTUser.NAME + ' Weight",' 36 | message+= '"state_topic": "' + MQTT_PREFIX + '/' + MQTTUser.NAME + '/weight",' 37 | message+= '"value_template": "{{ value_json.weight }}",' 38 | message+= '"json_attributes_topic": "' + MQTT_PREFIX + '/' + MQTTUser.NAME + '/weight",' 39 | message+= '"icon": "mdi:scale-bathroom",' 40 | message+= '"state_class": "measurement"}' 41 | publish.single( 42 | MQTT_DISCOVERY_PREFIX + '/sensor/' + MQTT_PREFIX + '/' + MQTTUser.NAME + '/config', 43 | message, 44 | retain=True, 45 | hostname=MQTT_HOST, 46 | port=MQTT_PORT, 47 | auth={'username':MQTT_USERNAME, 'password':MQTT_PASSWORD}, 48 | tls=MQTT_TLS 49 | ) 50 | logging.info(f"MQTT Discovery Setup Completed...") 51 | 52 | def check_weight(user, weight): 53 | return weight > user.GT and weight < user.LT 54 | 55 | def GetAge(d1): 56 | d1 = datetime.strptime(d1, "%Y-%m-%d") 57 | d2 = datetime.strptime(datetime.today().strftime('%Y-%m-%d'),'%Y-%m-%d') 58 | return abs((d2 - d1).days)/365 59 | 60 | def MQTT_publish(weight, unit, mitdatetime, hasImpedance, miimpedance): 61 | """Publishes weight data for the selected user""" 62 | if unit == "lbs": calcweight = round(weight * 0.4536, 2) 63 | if unit == "jin": calcweight = round(weight * 0.5, 2) 64 | if unit == "kg": calcweight = weight 65 | matcheduser = None 66 | for user in USERS: 67 | if(check_weight(user,weight)): 68 | matcheduser = user 69 | break 70 | if matcheduser is None: 71 | return 72 | height = matcheduser.HEIGHT 73 | age = GetAge(matcheduser.DOB) 74 | sex = matcheduser.SEX.lower() 75 | name = matcheduser.NAME 76 | 77 | lib = Xiaomi_Scale_Body_Metrics.bodyMetrics(calcweight, height, age, sex, 0) 78 | message = '{' 79 | message += '"weight":' + "{:.2f}".format(weight) 80 | message += ',"weight_unit":"' + str(unit) + '"' 81 | message += ',"bmi":' + "{:.2f}".format(lib.getBMI()) 82 | message += ',"basal_metabolism":' + "{:.2f}".format(lib.getBMR()) 83 | message += ',"visceral_fat":' + "{:.2f}".format(lib.getVisceralFat()) 84 | 85 | if hasImpedance: 86 | lib = Xiaomi_Scale_Body_Metrics.bodyMetrics(calcweight, height, age, sex, int(miimpedance)) 87 | bodyscale = ['Obese', 'Overweight', 'Thick-set', 'Lack-exercise', 'Balanced', 'Balanced-muscular', 'Skinny', 'Balanced-skinny', 'Skinny-muscular'] 88 | message += ',"lean_body_mass":' + "{:.2f}".format(lib.getLBMCoefficient()) 89 | message += ',"body_fat":' + "{:.2f}".format(lib.getFatPercentage()) 90 | message += ',"water":' + "{:.2f}".format(lib.getWaterPercentage()) 91 | message += ',"bone_mass":' + "{:.2f}".format(lib.getBoneMass()) 92 | message += ',"muscle_mass":' + "{:.2f}".format(lib.getMuscleMass()) 93 | message += ',"protein":' + "{:.2f}".format(lib.getProteinPercentage()) 94 | message += ',"body_type":"' + str(bodyscale[lib.getBodyType()]) + '"' 95 | message += ',"metabolic_age":' + "{:.0f}".format(lib.getMetabolicAge()) 96 | message += ',"impedance":' + "{:.0f}".format(int(miimpedance)) 97 | 98 | message += ',"timestamp":"' + mitdatetime + '"' 99 | message += '}' 100 | try: 101 | logging.info(f"Publishing data to topic {MQTT_PREFIX + '/' + name + '/weight'}: {message}") 102 | publish.single( 103 | MQTT_PREFIX + '/' + name + '/weight', 104 | message, 105 | retain=MQTT_RETAIN, 106 | hostname=MQTT_HOST, 107 | port=MQTT_PORT, 108 | auth={'username':MQTT_USERNAME, 'password':MQTT_PASSWORD}, 109 | tls=MQTT_TLS 110 | ) 111 | logging.info(f"Data Published ...") 112 | except Exception as error: 113 | logging.error(f"Could not publish to MQTT: {error}") 114 | raise 115 | 116 | 117 | 118 | # Configuraiton... 119 | # Trying To Load Config From options.json (HA Add-On) 120 | try: 121 | with open('/data/options.json') as json_file: 122 | sys.stdout.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - Loading Config From Add-On Options...\n") 123 | data = json.load(json_file) 124 | try: 125 | DEBUG_LEVEL = data["DEBUG_LEVEL"] 126 | if DEBUG_LEVEL not in ('CRITICAL','ERROR','WARNING','INFO','DEBUG','NOTSET'): 127 | DEBUG_LEVEL = DEFAULT_DEBUG_LEVEL 128 | logging.basicConfig(format='%(asctime)s - (%(levelname)s) %(message)s', level=DEBUG_LEVEL, datefmt='%Y-%m-%d %H:%M:%S') 129 | logging.info(f"-------------------------------------") 130 | logging.info(f"Starting Xiaomi mi Scale v{VERSION}...") 131 | logging.info(f"Loading Config From Options.json...") 132 | logging.warning(f"Invalid logging level provided, defaulting to {DEBUG_LEVEL}...") 133 | else: 134 | logging.basicConfig(format='%(asctime)s - (%(levelname)s) %(message)s', level=DEBUG_LEVEL, datefmt='%Y-%m-%d %H:%M:%S') 135 | logging.info(f"-------------------------------------") 136 | logging.info(f"Starting Xiaomi mi Scale v{VERSION}...") 137 | logging.info(f"Loading Config From Options.json...") 138 | logging.info(f"Logging Level Set to {DEBUG_LEVEL}...") 139 | # Prevent bleak log flooding 140 | bleak_logger = logging.getLogger("bleak") 141 | bleak_logger.setLevel(logging.INFO) 142 | except: 143 | DEBUG_LEVEL = DEFAULT_DEBUG_LEVEL 144 | logging.basicConfig(format='%(asctime)s - (%(levelname)s) %(message)s', level=DEBUG_LEVEL, datefmt='%Y-%m-%d %H:%M:%S') 145 | logging.info(f"-------------------------------------") 146 | logging.info(f"Starting Xiaomi mi Scale v{VERSION}...") 147 | logging.info(f"Loading Config From Options.json...") 148 | logging.info(f"No Logging Level Provided, Defaulting to {DEBUG_LEVEL}...") 149 | # Prevent bleak log flooding 150 | bleak_logger = logging.getLogger("bleak") 151 | bleak_logger.setLevel(logging.INFO) 152 | pass 153 | try: 154 | MISCALE_MAC = data["MISCALE_MAC"] 155 | logging.debug(f"MISCALE_MAC read from config: {MISCALE_MAC}") 156 | 157 | except: 158 | logging.error(f"MAC Address not provided...") 159 | raise 160 | try: 161 | MISCALE_VERSION = data["MISCALE_VERSION"] 162 | logging.info(f"MISCALE_VERSION option is deprecated and can safely be removed from config...") 163 | except: 164 | pass 165 | try: 166 | MQTT_USERNAME = data["MQTT_USERNAME"] 167 | logging.debug(f"MQTT_USERNAME read from config: {MQTT_USERNAME}") 168 | except: 169 | MQTT_USERNAME = "username" 170 | logging.debug(f"MQTT_USERNAME defaulted to: {MQTT_USERNAME}") 171 | pass 172 | try: 173 | MQTT_PASSWORD = data["MQTT_PASSWORD"] 174 | logging.debug(f"MQTT_PASSWORD read from config: ***") 175 | except: 176 | MQTT_PASSWORD = None 177 | logging.debug(f"MQTT_PASSWORD defaulted to: {MQTT_PASSWORD}") 178 | pass 179 | try: 180 | MQTT_HOST = data["MQTT_HOST"] 181 | logging.debug(f"MQTT_HOST read from config: {MQTT_HOST}") 182 | except: 183 | logging.error(f"MQTT Host not provided...") 184 | raise 185 | try: 186 | MQTT_RETAIN = data["MQTT_RETAIN"] 187 | logging.debug(f"MQTT_RETAIN read from config: {MQTT_RETAIN}") 188 | except: 189 | MQTT_RETAIN = True 190 | logging.debug(f"MQTT_RETAIN defaulted to: {MQTT_RETAIN}") 191 | pass 192 | try: 193 | MQTT_PORT = data["MQTT_PORT"] 194 | logging.debug(f"MQTT_PORT read from config: {MQTT_PORT}") 195 | if(type(MQTT_PORT) != int): 196 | logging.warning(f"Converting MQTT_PORT to integer...") 197 | MQTT_PORT = int(MQTT_PORT) 198 | except: 199 | MQTT_PORT = 1883 200 | logging.debug(f"MQTT_PORT defaulted to: {MQTT_PORT}") 201 | pass 202 | try: 203 | MQTT_TLS_CACERTS = data["MQTT_TLS_CACERTS"] 204 | logging.debug(f"MQTT_TLS_CACERTS read from config: {MQTT_TLS_CACERTS}") 205 | except: 206 | MQTT_TLS_CACERTS = None 207 | logging.debug(f"MQTT_TLS_CACERTS defaulted to: {MQTT_TLS_CACERTS}") 208 | pass 209 | try: 210 | MQTT_TLS_INSECURE = data["MQTT_TLS_INSECURE"] 211 | logging.debug(f"MQTT_TLS_INSECURE read from config: {MQTT_TLS_INSECURE}") 212 | except: 213 | MQTT_TLS_INSECURE = None 214 | logging.debug(f"MQTT_TLS_INSECURE defaulted to: {MQTT_TLS_INSECURE}") 215 | pass 216 | try: 217 | MQTT_PREFIX = data["MQTT_PREFIX"] 218 | logging.debug(f"MQTT_PREFIX read from config: {MQTT_PREFIX}") 219 | except: 220 | MQTT_PREFIX = "miscale" 221 | logging.debug(f"MQTT_PREFIX defaulted to: {MQTT_PREFIX}") 222 | pass 223 | try: 224 | TIME_INTERVAL = data["TIME_INTERVAL"] 225 | logging.info(f"TIME_INTERVAL option is deprecated and can safely be removed from config...") 226 | except: 227 | pass 228 | try: 229 | MQTT_DISCOVERY = data["MQTT_DISCOVERY"] 230 | logging.debug(f"MQTT_DISCOVERY read from config: {MQTT_DISCOVERY}") 231 | except: 232 | MQTT_DISCOVERY = True 233 | logging.debug(f"MQTT_DISCOVERY defaulted to: {MQTT_DISCOVERY}") 234 | pass 235 | try: 236 | MQTT_DISCOVERY_PREFIX = data["MQTT_DISCOVERY_PREFIX"] 237 | logging.debug(f"MQTT_DISCOVERY_PREFIX read from config: {MQTT_DISCOVERY_PREFIX}") 238 | except: 239 | if MQTT_DISCOVERY: 240 | logging.warning(f"MQTT Discovery enabled but no MQTT Prefix provided, defaulting to 'homeassistant'...") 241 | MQTT_DISCOVERY_PREFIX = "homeassistant" 242 | pass 243 | try: 244 | HCI_DEV = data["HCI_DEV"].lower() 245 | logging.debug(f"HCI_DEV read from config: {HCI_DEV}") 246 | except: 247 | HCI_DEV = "hci0" 248 | logging.debug(f"HCI_DEV defaulted to: {HCI_DEV}") 249 | pass 250 | try: 251 | BLUEPY_PASSIVE_SCAN = data["BLUEPY_PASSIVE_SCAN"] 252 | logging.debug(f"BLUEPY_PASSIVE_SCAN read from config: {BLUEPY_PASSIVE_SCAN}") 253 | except: 254 | BLUEPY_PASSIVE_SCAN = False 255 | logging.debug(f"BLUEPY_PASSIVE_SCAN defaulted to: {BLUEPY_PASSIVE_SCAN}") 256 | pass 257 | 258 | if MQTT_TLS_CACERTS in [None, '', 'Path to CA Cert File']: 259 | MQTT_TLS = None 260 | else: 261 | MQTT_TLS = {'ca_certs':MQTT_TLS_CACERTS, 'insecure':MQTT_TLS_INSECURE} 262 | 263 | USERS = [] 264 | for user in data["USERS"]: 265 | try: 266 | user = json.dumps(user) 267 | user = json.loads(user, object_hook=customUserDecoder) 268 | if user.GT > user.LT: 269 | raise ValueError("GT can not be larger than LT - user {user.Name}") 270 | USERS.append(user) 271 | except: 272 | logging.error(f"{sys.exc_info()[1]}") 273 | raise 274 | OLD_MEASURE = None 275 | logging.info(f"Config Loaded...") 276 | 277 | # Failed to open options.json 278 | except FileNotFoundError as error: 279 | DEBUG_LEVEL = DEFAULT_DEBUG_LEVEL 280 | logging.basicConfig(format='%(asctime)s - (%(levelname)s) %(message)s', level=DEBUG_LEVEL, datefmt='%Y-%m-%d %H:%M:%S') 281 | logging.info(f"-------------------------------------") 282 | logging.info(f"Starting Xiaomi mi Scale v{VERSION}...") 283 | logging.info(f"Loading Config From Options.json...") 284 | logging.error(f"options.json file missing... {error}") 285 | # Prevent bleak log flooding 286 | bleak_logger = logging.getLogger("bleak") 287 | bleak_logger.setLevel(logging.INFO) 288 | raise 289 | 290 | 291 | async def main(MISCALE_MAC): 292 | stop_event = asyncio.Event() 293 | 294 | # TODO: add something that calls stop_event.set() 295 | 296 | def callback(device, advertising_data): 297 | global OLD_MEASURE 298 | if device.address.lower() == MISCALE_MAC: 299 | logging.debug(f"miscale found, with advertising_data: {advertising_data}") 300 | try: 301 | ### Xiaomi V2 Scale ### 302 | data = binascii.b2a_hex(advertising_data.service_data['0000181b-0000-1000-8000-00805f9b34fb']).decode('ascii') 303 | logging.debug(f"miscale v2 found (service data: 0000181b-0000-1000-8000-00805f9b34fb)") 304 | data = "1b18" + data # Remnant from previous code. Needs to be cleaned in the future 305 | data2 = bytes.fromhex(data[4:]) 306 | ctrlByte1 = data2[1] 307 | isStabilized = ctrlByte1 & (1<<5) 308 | hasImpedance = ctrlByte1 & (1<<1) 309 | measunit = data[4:6] 310 | measured = int((data[28:30] + data[26:28]), 16) * 0.01 311 | unit = '' 312 | if measunit == "03": unit = 'lbs' 313 | if measunit == "02": unit = 'kg' ; measured = measured / 2 314 | miimpedance = str(int((data[24:26] + data[22:24]), 16)) 315 | if unit and isStabilized: 316 | if OLD_MEASURE != round(measured, 2) + int(miimpedance): 317 | OLD_MEASURE = round(measured, 2) + int(miimpedance) 318 | MQTT_publish(round(measured, 2), unit, str(datetime.now().strftime('%Y-%m-%dT%H:%M:%S+00:00')), hasImpedance, miimpedance) 319 | except: 320 | pass 321 | try: 322 | ### Xiaomi V1 Scale ### 323 | data = binascii.b2a_hex(advertising_data.service_data['0000181d-0000-1000-8000-00805f9b34fb']).decode('ascii') 324 | logging.debug(f"miscale v1 found (service data: 0000181d-0000-1000-8000-00805f9b34fb)") 325 | data = "1d18" + data # Remnant from previous code. Needs to be cleaned in the future 326 | measunit = data[4:6] 327 | measured = int((data[8:10] + data[6:8]), 16) * 0.01 328 | unit = '' 329 | if measunit.startswith(('03', 'a3')): unit = 'lbs' 330 | if measunit.startswith(('12', 'b2')): unit = 'jin' 331 | if measunit.startswith(('22', 'a2')): unit = 'kg' ; measured = measured / 2 332 | if unit: 333 | if OLD_MEASURE != round(measured, 2): 334 | OLD_MEASURE = round(measured, 2) 335 | MQTT_publish(round(measured, 2), unit, str(datetime.now().strftime('%Y-%m-%dT%H:%M:%S+00:00')), "", "") 336 | except: 337 | pass 338 | pass 339 | 340 | async with BleakScanner( 341 | callback, 342 | device=f"{HCI_DEV}" 343 | ) as scanner: 344 | ... 345 | # Important! Wait for an event to trigger stop, otherwise scanner 346 | # will stop immediately. 347 | await stop_event.wait() 348 | 349 | 350 | if __name__ == "__main__": 351 | if MQTT_DISCOVERY: 352 | MQTT_discovery() 353 | logging.info(f"-------------------------------------") 354 | logging.info(f"Initialization completed, step on scale to wake it up and get a weight value sent... Make sure the scale is within reach...") 355 | try: 356 | asyncio.run(main(MISCALE_MAC.lower())) 357 | except Exception as error: 358 | logging.error(f"Unable to connect to Bluetooth: {error}") 359 | pass 360 | --------------------------------------------------------------------------------