From 4519d1ece808b0fca313000175e3fb687761cb7a Mon Sep 17 00:00:00 2001 From: parra Date: Thu, 11 Aug 2022 18:12:20 +0200 Subject: [PATCH] Initial commit --- .gitignore | 4 ++ README.md | 34 ++++++++++++ env_file | 22 ++++++++ main.py | 142 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 + 5 files changed, 205 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 env_file create mode 100755 main.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..66925f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.bak +unstable-* +*.log +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..e18b612 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Bluetooth LE to MQTT bridge for the Xiaomi Mijia Temperature & Humidity sensor + +## Create environment file + +Copy the `env_file` to `.env` and open it. Complete all environment variables: + +- `MQTT_[SERVER/PORT/USER/PASSWORD/CLIENT_ID]` are used to connect with the broker. +- `MQTT_TOPIC_PREFIX` define the prefix for all topics with sensor info. +- `MQTT_TELE_PREFIX` is used to publish the sensor data, like battery or status. +- `MQTT_SENSOR_NAME` contains the sensor name, useful to split the telemetry data if you have more than one sensors. +- `MQTT_PUBLISH_DELAY` specify, in seconds, how many time should wait since the script take the measurements to publish in the broker +- `MIJIA_BTLE_ADDRESS` constant with the BLE address of your Mijia device.. This can be retrieved activating the pairing mode in the sensor and scanning the BT devices + +## Install dependencies + +You'll need to install bluez and python3. Then you'll need pip3 to install bluepy. + +Example on a Raspberry Pi 3: +```sh +$ sudo apt-get install python-pip libglib2.0-dev +$ sudo pip3 install -r requirements.txt +``` + +## Run + +You can execute the script directly using the command: +```sh +$ ./main.py +``` + +Or you can add a new entry in the `crontab`, like: +```sh +*/20 * * * * /usr/bin/python3 ~/scripts/mijia-temperature/main.py >~/scripts/mijia-temperature/last.log 2>&1 +``` diff --git a/env_file b/env_file new file mode 100644 index 0000000..e14fbdd --- /dev/null +++ b/env_file @@ -0,0 +1,22 @@ +# Broker configuration +MQTT_SERVER= +MQTT_PORT= +MQTT_USER= +MQTT_PASSWORD= +MQTT_CLIENT_ID= + +# Topic configuration +MQTT_TOPIC_PREFIX=home/sensor +MQTT_TELE_PREFIX=home/tele + +MQTT_SENSOR_NAME=mijia-salon + +MQTT_TOPIC_HUMIDITY=${MQTT_TOPIC_PREFIX}/humedad +MQTT_TOPIC_TEMPERATURE=${MQTT_TOPIC_PREFIX}/temperatura +MQTT_TOPIC_BATTERY=${MQTT_TELE_PREFIX}/${MQTT_SENSOR_NAME}/bateria +MQTT_TOPIC_STATE=${MQTT_TELE_PREFIX}/${MQTT_SENSOR_NAME}/event + +MQTT_PUBLISH_DELAY=5 + +# Sensor configuration +MIJIA_BTLE_ADDRESS= \ No newline at end of file diff --git a/main.py b/main.py new file mode 100755 index 0000000..c8dc83b --- /dev/null +++ b/main.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 + +"""MiJia GATT to MQTT""" + +import os +import re +import time + +from dotenv import load_dotenv +import paho.mqtt.client as mqtt +from bluepy import btle + +load_dotenv() # Cargamos las variables de entorno necesarias + +MQTT_TOPIC_HUMIDITY = os.getenv('MQTT_TOPIC_HUMIDITY') +MQTT_TOPIC_TEMPERATURE = os.getenv('MQTT_TOPIC_TEMPERATURE') +MQTT_TOPIC_BATTERY = os.getenv('MQTT_TOPIC_BATTERY') +MQTT_TOPIC_STATE = os.getenv('MQTT_TOPIC_STATE') + +MQTT_PUBLISH_DELAY = int(os.getenv('MQTT_PUBLISH_DELAY')) +MQTT_CLIENT_ID = os.getenv('MQTT_CLIENT_ID') + +MQTT_SERVER = os.getenv('MQTT_SERVER') +MQTT_PORT = int(os.getenv('MQTT_PORT')) +MQTT_USER = os.getenv('MQTT_USER') +MQTT_PASSWORD = os.getenv('MQTT_PASSWORD') + +MIJIA_BTLE_ADDRESS = os.getenv('MIJIA_BTLE_ADDRESS') + +MIJIA_BATTERY_SERVICE_UUID = btle.UUID('180f') +MIJIA_BATTERY_CHARACTERISTIC_UUID = btle.UUID('2a19') + +MIJIA_DATA_SERVICE_UUID = btle.UUID('226c0000-6476-4566-7562-66734470666d') +MIJIA_DATA_CHARACTERISTIC_UUID = btle.UUID('226caa55-6476-4566-7562-66734470666d') +MIJIA_DATA_CHARACTERISTIC_HANDLE = 0x0010 + +BTLE_SUBSCRIBE_VALUE = bytes([0x01, 0x00]) +BTLE_UNSUBSCRIBE_VALUE = bytes([0x00, 0x00]) + +battery = None +temperature = None +humidity = None + + +def on_connect(client, userdata, flags, rc): + client.publish(MQTT_TOPIC_STATE, 'connected', 1, True) + + +class MyDelegate(btle.DefaultDelegate): + def __init__(self): + btle.DefaultDelegate.__init__(self) + + def handleNotification(self, cHandle, data): + fetch_sensor_data(bytearray(data).decode('utf-8')) + + +def main(): + mqttc = mqtt.Client(MQTT_CLIENT_ID) + mqttc.username_pw_set(MQTT_USER, MQTT_PASSWORD) + mqttc.will_set(MQTT_TOPIC_STATE, 'disconnected', 1, True) + mqttc.on_connect = on_connect + + mqttc.connect(MQTT_SERVER, MQTT_PORT, 60) + mqttc.loop_start() + + last_msg_time = time.time() + + while True: + try: + print('Connecting to ' + MIJIA_BTLE_ADDRESS) + dev = btle.Peripheral(MIJIA_BTLE_ADDRESS) + print('Set delegate') + dev.setDelegate(MyDelegate()) + + # Get battery level + if battery is None: + fetch_battery_level(dev) + print('Battery level: ' + str(battery)) + + # Subscribe to data characteristic + if temperature is None or humidity is None: + dev.writeCharacteristic(MIJIA_DATA_CHARACTERISTIC_HANDLE, BTLE_SUBSCRIBE_VALUE, True) + while True: + if dev.waitForNotifications(1.0): + print('Temperature: ' + temperature) + print('Humidity: ' + humidity) + dev.writeCharacteristic(MIJIA_DATA_CHARACTERISTIC_HANDLE, BTLE_UNSUBSCRIBE_VALUE, True) + dev.disconnect() + break + + if battery is not None and temperature is not None and humidity is not None: + delay_gap = time.time() - last_msg_time + if delay_gap < MQTT_PUBLISH_DELAY: + time.sleep(MQTT_PUBLISH_DELAY - delay_gap) + + publish_sensor_data(mqttc) + last_msg_time = time.time() + reset_variables() + break + + except (btle.BTLEDisconnectError, IOError): + print("Disconnected :(") + +def reset_variables(): + global battery + global temperature + global humidity + + battery = None + temperature = None + humidity = None + + +def fetch_battery_level(dev): + global battery + + battery_service = dev.getServiceByUUID(MIJIA_BATTERY_SERVICE_UUID) + battery_characteristic = battery_service.getCharacteristics(MIJIA_BATTERY_CHARACTERISTIC_UUID)[0] + battery = ord(battery_characteristic.read()) + + +def fetch_sensor_data(temp_hum): + global temperature + global humidity + + pattern = re.compile('T=([\d.-]+) H=([\d.-]+)') + match = re.match(pattern, temp_hum) + if match: + temperature = match.group(1) + humidity = match.group(2) + + +def publish_sensor_data(mqttc): + mqttc.publish(MQTT_TOPIC_TEMPERATURE, temperature, 1, True) + mqttc.publish(MQTT_TOPIC_HUMIDITY, humidity, 1, True) + mqttc.publish(MQTT_TOPIC_BATTERY, battery, 1, True) + time.sleep(MQTT_PUBLISH_DELAY) + + +if __name__ == '__main__': + print('Starting MiJia GATT client') + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..847db70 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +bluepy==1.3.0 +paho-mqtt==1.6.1 +python-dotenv==0.20.0