From 4519d1ece808b0fca313000175e3fb687761cb7a Mon Sep 17 00:00:00 2001
From: parra <victor+git@parravidales.es>
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