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