├── .gitignore ├── AUTHORS ├── Dockerfile ├── LICENSE ├── README.md ├── config.ini.dist ├── demo.gif ├── miflora-mqtt-daemon.py ├── requirements.txt └── template.service /.gitignore: -------------------------------------------------------------------------------- 1 | # Project specific 2 | 3 | config.ini 4 | 5 | 6 | # Created by https://www.gitignore.io/api/python 7 | 8 | ### Python ### 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | env/ 20 | build/ 21 | develop-eggs/ 22 | dist/ 23 | downloads/ 24 | eggs/ 25 | .eggs/ 26 | lib/ 27 | lib64/ 28 | parts/ 29 | sdist/ 30 | var/ 31 | wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | 36 | # pyCharm 37 | .idea/ 38 | 39 | # PyInstaller 40 | # Usually these files are written by a python script from a template 41 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 42 | *.manifest 43 | *.spec 44 | 45 | # Installer logs 46 | pip-log.txt 47 | pip-delete-this-directory.txt 48 | 49 | # Unit test / coverage reports 50 | htmlcov/ 51 | .tox/ 52 | .coverage 53 | .coverage.* 54 | .cache 55 | nosetests.xml 56 | coverage.xml 57 | *,cover 58 | .hypothesis/ 59 | 60 | # Translations 61 | *.mo 62 | *.pot 63 | 64 | # Django stuff: 65 | *.log 66 | local_settings.py 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # celery beat schedule file 88 | celerybeat-schedule 89 | 90 | # SageMath parsed files 91 | *.sage.py 92 | 93 | # dotenv 94 | .env 95 | 96 | # virtualenv 97 | .venv 98 | venv/ 99 | ENV/ 100 | 101 | # Spyder project settings 102 | .spyderproject 103 | .spyproject 104 | 105 | # Rope project settings 106 | .ropeproject 107 | 108 | # mkdocs documentation 109 | /site 110 | 111 | # End of https://www.gitignore.io/api/python 112 | 113 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Jan Willhaus 2 | Thomas Dietrich 3 | Wolfgang Gaar 4 | Sebastian Raff 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-stretch 2 | MAINTAINER Lars von Wedel 3 | 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | 7 | RUN apt-get update && apt-get install -y bluez 8 | 9 | COPY requirements.txt requirements.txt 10 | RUN pip install --upgrade pip 11 | RUN pip install --no-cache-dir -r requirements.txt 12 | 13 | COPY . . 14 | 15 | CMD [ "python3", "./miflora-mqtt-daemon.py", "--config_dir", "/config" ] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jan Willhaus 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xiaomi Mi Bluetooth Sensor MQTT Client/Daemon 2 | 3 | A simple Linux python script to query arbitrary Mi Bluetooth sensor devices and send the data to an **MQTT** broker, 4 | e.g., the famous [Eclipse Mosquitto](https://projects.eclipse.org/projects/technology.mosquitto). 5 | After data made the hop to the MQTT broker it can be used by home automation software, like [openHAB](https://openhab.org) or Home Assistant. 6 | 7 | ![Demo gif for command line execution](demo.gif) 8 | 9 | The program can be executed in **daemon mode** to run continuously in the background, e.g., as a systemd service. 10 | 11 | ## About Mi Flora 12 | * [Xiaomi Mi Flora sensors](https://www.huahuacaocao.com) ([e.g. 12-17€](https://www.aliexpress.com/wholesale?SearchText=xiaomi+mi+flora+plant+sensor)) are meant to keep your plants alive by monitoring soil moisture, soil conductivity and light conditions 13 | * The sensor uses Bluetooth Low Energy (BLE) and has a rather limited range 14 | * A coin cell battery is used as power source, which should last between 1.5 to 2 years under normal conditions 15 | * Food for thought: The sensor can also be used for other things than plants, like in the [fridge](https://community.openhab.org/t/refrigerator-temperature-sensors/40076) or as [door and blind sensor](https://community.openhab.org/t/miflora-cheap-window-and-door-sensor-water-sensor-blind-sensor-etc/38232) 16 | 17 | ![Promotional image](https://ae01.alicdn.com/kf/HTB1WAd1XEvrK1RjSszfq6xJNVXaB/International-Version-Original-Xiaomi-Flower-Care-Soil-Water-Light-Smart-Flower-Monitor-for-Garden-Plants.jpg_640x640.jpg) 18 | 19 | ## About Xiaomi Mijia Temperature and Humidity Sensor 20 | * ''Xiaomi Mijia Temperature and Humidity Sensor'' ([e.g. $13](https://www.aliexpress.com/wholesale?SearchText=Mijia+Bluetooth+Temperature+Humidity+Sensor)) are for monitoring indoor air temperature and humidity 21 | * The sensor uses Bluetooth Low Energy (BLE) and has a rather limited range 22 | * Weight: 43 g 23 | * Screen size: 1.78 inch 24 | * Temperature range: -9.9°C-60°C 25 | * Humidity range: 0~99.9% 26 | * Rated power: 0.18 mW 27 | * Powered By: Battery (AAA) 28 | 29 | ![Promotional image](http://ae01.alicdn.com/kf/HTB11qrpeStYBeNjSspkq6zU8VXax.jpg) 30 | 31 | ## Features 32 | 33 | * Tested with Mi Flora firmware v2.6.2, v2.6.4, v2.6.6, v3.1.4, others anticipated 34 | * Tested with Xiaomi Mijia Temperature and Humidity Sensor (MJ_HT_V1) firmware v00.00.66 35 | * Build on top of [open-homeautomation/miflora](https://github.com/open-homeautomation/miflora) and [mitemp_bt](https://github.com/flavio20002/mitemp_bt) 36 | * Highly configurable 37 | * Data publication via MQTT 38 | * Configurable topic and payload: 39 | * JSON encoded 40 | * following the [Homie Convention v2.0.5](https://github.com/marvinroger/homie) 41 | * following the [mqtt-smarthome architecture proposal](https://github.com/mqtt-smarthome/mqtt-smarthome) 42 | * using the [HomeAssistant MQTT discovery format](https://home-assistant.io/docs/mqtt/discovery/) 43 | * using the [ThingsBoard.io](https://thingsboard.io/) MQTT interface 44 | * following the [Wiren Board MQTT Conventions](https://github.com/contactless/homeui/blob/master/conventions.md) 45 | * Announcement messages to support auto-discovery services 46 | * MQTT authentication support 47 | * No special/root privileges needed 48 | * Daemon mode (default) 49 | * Systemd service, sd\_notify messages generated 50 | * MQTT-less mode, printing data directly to stdout/file 51 | * Automatic generation of openHAB items and rules 52 | * Reliable and intuitive 53 | * Tested on Raspberry Pi 3 and Raspberry Pi 0W 54 | * Wiren Board 5 (Debian Stretch) 55 | 56 | ### Readings 57 | 58 | The Mi Flora sensor offers the following plant and soil readings: 59 | 60 | | Name | Description | 61 | |-----------------|-------------| 62 | | `temperature` | Air temperature, in [°C] (0.1°C resolution) | 63 | | `light` | [Sunlight intensity](https://aquarium-digest.com/tag/lumenslux-requirements-of-a-cannabis-plant/), in [lux] | 64 | | `moisture` | [Soil moisture](https://observant.zendesk.com/hc/en-us/articles/208067926-Monitoring-Soil-Moisture-for-Optimal-Crop-Growth), in [%] | 65 | | `conductivity` | [Soil fertility](https://www.plantcaretools.com/measure-fertilization-with-ec-meters-for-plants-faq), in [µS/cm] | 66 | | `battery` | Sensor battery level, in [%] | 67 | 68 | The Xiaomi Mijia Temperature and Humidity Sensor offers the following readings: 69 | 70 | | Name | Description | 71 | |-----------------|-------------| 72 | | `temperature` | Air temperature, in [°C] (0.1°C resolution) | 73 | | `humidity` | Air humidity in [%] | 74 | | `battery` | Sensor battery level, in [%] | 75 | 76 | ## Prerequisites 77 | 78 | An MQTT broker is needed as the counterpart for this daemon. 79 | Even though an MQTT-less mode is provided, it is not recommended for normal smart home automation integration. 80 | MQTT is huge help in connecting different parts of your smart home and setting up of a broker is quick and easy. 81 | 82 | ## Installation 83 | 84 | On a modern Linux system just a few steps are needed to get the daemon working. 85 | The following example shows the installation under Debian/Raspbian below the `/opt` directory: 86 | 87 | ```shell 88 | sudo apt install git python3 python3-pip bluetooth bluez 89 | 90 | git clone https://github.com/ThomDietrich/miflora-mqtt-daemon.git /opt/miflora-mqtt-daemon 91 | 92 | cd /opt/miflora-mqtt-daemon 93 | sudo pip3 install -r requirements.txt 94 | ``` 95 | 96 | The daemon depends on `gatttool`, an external tool provided by the package `bluez` installed just now. 97 | Make sure gatttool is available on your system by executing the command once: 98 | 99 | ```shell 100 | gatttool --help 101 | ``` 102 | 103 | ## Configuration 104 | 105 | To match personal needs, all operation details can be configured using the file [`config.ini`](config.ini.dist). 106 | The file needs to be created first: 107 | 108 | ```shell 109 | cp /opt/miflora-mqtt-daemon/config.{ini.dist,ini} 110 | vim /opt/miflora-mqtt-daemon/config.ini 111 | ``` 112 | 113 | **Attention:** 114 | You need to add at least one sensor to the configuration. 115 | Scan for available Mi Bluetooth sensors in your proximity with the command: 116 | 117 | ```shell 118 | sudo hcitool lescan 119 | ``` 120 | 121 | Interfacing your Mi Bluetooth sensor with this program is harmless. 122 | The device will not be modified and will still work with the official Xiaomi app. 123 | 124 | ## Execution 125 | 126 | A first test run is as easy as: 127 | 128 | ```shell 129 | python3 /opt/miflora-mqtt-daemon/miflora-mqtt-daemon.py 130 | ``` 131 | 132 | With a correct configuration the result should look similar to the the screencap above. 133 | Pay attention to communication errors due to distance related weak BLE connections. 134 | 135 | Using the command line argument `--config`, a directory where to read the config.ini file from can be specified, e.g. 136 | 137 | ```shell 138 | python3 /opt/miflora-mqtt-daemon/miflora-mqtt-daemon.py --config /opt/miflora-config 139 | ``` 140 | 141 | The extensive output can be reduced to error messages: 142 | 143 | ```shell 144 | python3 /opt/miflora-mqtt-daemon/miflora-mqtt-daemon.py > /dev/null 145 | ``` 146 | 147 | ### Continuous Daemon/Service 148 | 149 | You most probably want to execute the program **continuously in the background**. 150 | This can be done either by using the internal daemon or cron. 151 | 152 | **Attention:** Daemon mode must be enabled in the configuration file (default). 153 | 154 | 1. Systemd service - on systemd powered systems the **recommended** option 155 | 156 | ```shell 157 | sudo cp /opt/miflora-mqtt-daemon/template.service /etc/systemd/system/miflora.service 158 | 159 | sudo systemctl daemon-reload 160 | 161 | sudo systemctl start miflora.service 162 | sudo systemctl status miflora.service 163 | 164 | sudo systemctl enable miflora.service 165 | ``` 166 | 167 | 1. Screen Shell - Run the program inside a [screen shell](https://www.howtoforge.com/linux_screen): 168 | 169 | ```shell 170 | screen -S miflora-mqtt-daemon -d -m python3 /path/to/miflora-mqtt-daemon.py 171 | ``` 172 | 173 | ## Usage with Docker 174 | 175 | A Dockerfile in the repository can be used to build a docker container from the sources with a command such as: 176 | 177 | ```shell 178 | docker build -t miflora-mqtt-daemon . 179 | ``` 180 | 181 | Running the container in interactive mode works like this: 182 | 183 | ```shell 184 | docker run -it --name miflora-mqtt-daemon -v .:/config miflora-mqtt-daemon 185 | ``` 186 | 187 | To run the container in daemon mode use `-d` flag: 188 | 189 | ```shell 190 | docker run -d --name miflora-mqtt-daemon -v .:/config miflora-mqtt-daemon 191 | ``` 192 | 193 | The `/config` volume can be used to provide a directory on the host which contains your `config.ini` file (e.g. the `.` in the above example could represent `/opt/miflora-mqtt-daemon`). 194 | You may need to tweak the network settings (e.g. `--network host`) for Docker depending on how your system is set up. 195 | 196 | ## Integration 197 | 198 | In the "mqtt-json" reporting mode, data will be published to the MQTT broker topic "`miflora/sensorname`" (e.g. `miflora/petunia`, names configurable). 199 | An example: 200 | 201 | ```json 202 | {"light": 5424, "moisture": 30, "temperature": 21.4, "conductivity": 1020, "battery": 100} 203 | ``` 204 | 205 | This data can be subscribed to and processed by other applications, like [openHAB](https://openhab.org). 206 | 207 | Enjoy! 208 | 209 | 210 | ### openHAB 211 | 212 | To make further processing of the sensor readings as easy as possible, the program has an integrated generator for openHAB Items definitions. 213 | To generate a complete listing of Items, which you can then copy and adapt to your openHAB setup, execute: 214 | 215 | ```shell 216 | python3 /opt/miflora-mqtt-daemon/miflora-mqtt-daemon.py --gen-openhab 217 | ``` 218 | 219 | The following code snippet shows a simple example of how a Mi Flora openHAB Items file could look like for the above example: 220 | 221 | ```java 222 | // miflora.items 223 | 224 | // Mi Flora specific groups 225 | Group gBattery "Mi Flora sensor battery level elements" (gAll) 226 | Group gTemperature "Mi Flora air temperature elements" (gAll) 227 | Group gMoisture "Mi Flora soil moisture elements" (gAll) 228 | Group gConductivity "Mi Flora soil conductivity/fertility elements" (gAll) 229 | Group gLight "Mi Flora sunlight intensity elements" (gAll) 230 | 231 | // Mi Flora "Big Blue Petunia" (C4:7C:8D:60:DC:E6) 232 | Number Balcony_Petunia_Battery "Balcony Petunia Sensor Battery Level [%d %%]" (gBalcony, gBattery) {mqtt="<[broker:miflora/petunia:state:JSONPATH($.battery)]"} 233 | Number Balcony_Petunia_Temperature "Balcony Petunia Air Temperature [%.1f °C]" (gBalcony, gTemperature) {mqtt="<[broker:miflora/petunia:state:JSONPATH($.temperature)]"} 234 | Number Balcony_Petunia_Moisture "Balcony Petunia Soil Moisture [%d %%]" (gBalcony, gMoisture) {mqtt="<[broker:miflora/petunia:state:JSONPATH($.moisture)]"} 235 | Number Balcony_Petunia_Conductivity "Balcony Petunia Soil Conductivity/Fertility [%d µS/cm]" (gBalcony, gConductivity) {mqtt="<[broker:miflora/petunia:state:JSONPATH($.conductivity)]"} 236 | Number Balcony_Petunia_Light "Balcony Petunia Sunlight Intensity [%d lux]" (gBalcony, gLight) {mqtt="<[broker:miflora/petunia:state:JSONPATH($.light)]"} 237 | ``` 238 | 239 | Paste the presented items definition into an openHAB items file and you are ready to go. 240 | Be sure to install the used MQTT Binding and JSONPath Transformation openHAB addons beforehand. 241 | 242 | ### ThingsBoard 243 | 244 | to integrate with [ThingsBoard.io](https://thingsboard.io/): 245 | 246 | 1. in your `config.ini` set `reporting_method = thingsboard-json` 247 | 1. in your `config.ini` assign unique sensor names for your plants 248 | 1. on the ThingsBoard platform create devices and use `Access token` as `Credential type` and the chosen sensor name as token 249 | 250 | ### Wiren Board 251 | 252 | to integrate with [Wiren Board](https://wirenboard.com/en/) in your `config.ini` set: 253 | 254 | 1. `reporting_method = wirenboard-mqtt` 255 | 1. set `hostname` with address of [Wiren Board](https://wirenboard.com/en/) controller and optionally `username` and `password` 256 | 257 | sensors will automatically appear on [Wiren Board](https://wirenboard.com/en/) as separate devices 258 | 259 | ---- 260 | 261 | #### Disclaimer and Legal 262 | 263 | > *Xiaomi* and *Mi Flora* are registered trademarks of *BEIJING XIAOMI TECHNOLOGY CO., LTD.* 264 | > 265 | > This project is a community project not for commercial use. 266 | > The authors will not be held responsible in the event of device failure or withered plants. 267 | > 268 | > This project is in no way affiliated with, authorized, maintained, sponsored or endorsed by *Xiaomi* or any of its affiliates or subsidiaries. 269 | -------------------------------------------------------------------------------- /config.ini.dist: -------------------------------------------------------------------------------- 1 | # Configuration file for Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon 2 | # Source: https://github.com/ThomDietrich/miflora-mqtt-daemon 3 | # 4 | # Uncomment and adapt all settings as needed. 5 | 6 | [General] 7 | 8 | # The operation mode of the program. Determines wether retrieved sensor data is published via MQTT or stdout/file. 9 | # Currently supported: 10 | # 11 | # mqtt-json - Publish to an MQTT broker in a proprietary json format (Default) 12 | # mqtt-homie - Publish to an MQTT broker following the Homie MQTT convention 13 | # (https://github.com/marvinroger/homie) 14 | # mqtt-smarthome - Publish to an MQTT broker following the mqtt-smarthome proposal 15 | # (https://github.com/mqtt-smarthome/mqtt-smarthome) 16 | # homeassistant-mqtt - Publish to an MQTT broker following the HomeAssistant discovery format 17 | # (https://www.home-assistant.io/docs/mqtt/discovery/) 18 | # thingsboard-json - Publish to the ThingsBoard MQTT broker 19 | # (https://thingsboard.io) 20 | # wirenboard-mqtt - Publish to the Wiren Board MQTT broker 21 | # (https://wirenboard.com) 22 | # json - Print to stdout as json encoded strings 23 | # 24 | #reporting_method = mqtt-json 25 | 26 | # The bluetooth adapter that should be used to connect to Mi Bluetooth devices (Default: hci0) 27 | #adapter = hci0 28 | 29 | [Daemon] 30 | 31 | # Enable or Disable an endless execution loop (Default: true) 32 | #enabled = true 33 | 34 | # The period between two measurements in seconds for MiFlora sensors (Default: 300) 35 | #period_miflora = 300 36 | 37 | # The period between two measurements in seconds for MiTempBt sensors (Default: 60) 38 | #period_mitempbt = 60 39 | 40 | [MQTT] 41 | 42 | # The hostname or IP address of the MQTT broker to connect to (Default: localhost) 43 | #hostname = localhost 44 | 45 | # The TCP port the MQTT broker is listening on (Default: 1883) 46 | #port = 1883 47 | 48 | # Maximum period in seconds between ping messages to the broker. (Default: 60) 49 | #keepalive = 60 50 | 51 | # The MQTT base topic to publish all Mi Flora sensor data topics under. 52 | # Default depends on the configured reporting_method 53 | #base_topic = misensor # Default for: mqtt-json, mqtt-smarthome 54 | #base_topic = homie # Default for: mqtt-homie 55 | #base_topic = homeassistant # Default for: homeassistant-mqtt 56 | #base_topic = v1/devices/me/telemetry # Default for: thingsboard-json 57 | #base_topic = # Default for: wirenboard-mqtt 58 | 59 | # Homie specific: The device ID for this daemon instance (Default: miflora-mqtt-daemon) 60 | #homie_device_id = miflora-mqtt-daemon 61 | 62 | # The MQTT broker authentification credentials (Default: no authentication) 63 | #username = user 64 | #password = pwd123 65 | 66 | # Enable TLS/SSL on the connection 67 | #tls = false 68 | 69 | # Path to CA Certificate file to verify host 70 | #tls_ca_cert = 71 | 72 | # Path to TLS client auth key file 73 | #tls_keyfile = 74 | 75 | # Path to TLS client auth certificate file 76 | #tls_certfile = 77 | 78 | [MiFlora] 79 | 80 | # Add your Mi Flora sensors here. Each sensor consists of a name and a Ethernet MAC address. 81 | # Additional location information can be added to the name, delimited by an '@'. 82 | # Scan for sensors from the command line with: 83 | # $ sudo hcitool lescan 84 | # 85 | # Examples: 86 | # 87 | #Schefflera@Living = C4:7C:8D:11:22:33 88 | #JapaneseBonsai = C4:7C:8D:44:55:66 89 | #Petunia@Balcony = C4:7C:8D:77:88:99 90 | 91 | [MiTempBt] 92 | 93 | # Add your Mi Temerature & Humidity sensors here. Setup is same as for [MiFlora] section 94 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aqualx/miflora-mqtt-daemon/1c928dc2033ce20ade952c28a069dfa4439bad17/demo.gif -------------------------------------------------------------------------------- /miflora-mqtt-daemon.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import ssl 4 | import sys 5 | import re 6 | import json 7 | import os.path 8 | import argparse 9 | import threading 10 | from itertools import chain 11 | from time import time, sleep, localtime, strftime 12 | from collections import OrderedDict 13 | from colorama import init as colorama_init 14 | from colorama import Fore, Back, Style 15 | from configparser import ConfigParser 16 | from unidecode import unidecode 17 | from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE 18 | from mithermometer.mithermometer_poller import MiThermometerPoller, MI_HUMIDITY 19 | from btlewrap import BluepyBackend, BluetoothBackendException 20 | from bluepy.btle import BTLEException 21 | import paho.mqtt.client as mqtt 22 | import sdnotify 23 | from signal import signal, SIGPIPE, SIG_DFL 24 | 25 | signal(SIGPIPE,SIG_DFL) 26 | 27 | project_name = 'Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon' 28 | project_url = 'https://github.com/ThomDietrich/miflora-mqtt-daemon' 29 | 30 | sensor_name_miflora = "Mi Flora" 31 | sensor_type_miflora = "MiFlora" 32 | sensor_name_mitempbt = "Mijia Bluetooth Temperature Smart Humidity" 33 | sensor_type_mitempbt = "MiTempBt" 34 | 35 | miflora_parameters = OrderedDict([ 36 | (MI_LIGHT, dict(name="LightIntensity", name_pretty='Sunlight Intensity', typeformat='%d', unit='lux', device_class="illuminance")), 37 | (MI_TEMPERATURE, dict(name="AirTemperature", name_pretty='Air Temperature', typeformat='%.1f', unit='°C', device_class="temperature")), 38 | (MI_MOISTURE, dict(name="SoilMoisture", name_pretty='Soil Moisture', typeformat='%d', unit='%', device_class="humidity")), 39 | (MI_CONDUCTIVITY, dict(name="SoilConductivity", name_pretty='Soil Conductivity/Fertility', typeformat='%d', unit='µS/cm')), 40 | (MI_BATTERY, dict(name="Battery", name_pretty='Sensor Battery Level', typeformat='%d', unit='%', device_class="battery")) 41 | ]) 42 | 43 | mitempbt_parameters = OrderedDict([ 44 | (MI_TEMPERATURE, dict(name="AirTemperature", name_pretty='Air Temperature', typeformat='%.1f', unit='°C', device_class="temperature")), 45 | (MI_HUMIDITY, dict(name="Humidity", name_pretty='Air Moisture', typeformat='%d', unit='%', device_class="humidity")), 46 | (MI_BATTERY, dict(name="Battery", name_pretty='Sensor Battery Level', typeformat='%d', unit='%', device_class="battery")) 47 | ]) 48 | 49 | if False: 50 | # will be caught by python 2.7 to be illegal syntax 51 | print('Sorry, this script requires a python3 runtime environment.', file=sys.stderr) 52 | 53 | # Argparse 54 | parser = argparse.ArgumentParser(description=project_name, epilog='For further details see: ' + project_url) 55 | parser.add_argument('--gen-openhab', help='generate openHAB items based on configured sensors', action='store_true') 56 | parser.add_argument('--config_dir', help='set directory where config.ini is located', default=sys.path[0]) 57 | parse_args = parser.parse_args() 58 | 59 | # Intro 60 | colorama_init() 61 | print(Fore.GREEN + Style.BRIGHT) 62 | print(project_name) 63 | print('Source:', project_url) 64 | print(Style.RESET_ALL) 65 | 66 | # Systemd Service Notifications - https://github.com/bb4242/sdnotify 67 | sd_notifier = sdnotify.SystemdNotifier() 68 | 69 | # Logging function 70 | def print_line(text, error = False, warning=False, sd_notify=False, console=True): 71 | timestamp = strftime('%Y-%m-%d %H:%M:%S', localtime()) 72 | if console: 73 | if error: 74 | print(Fore.RED + Style.BRIGHT + '[{}] '.format(timestamp) + Style.RESET_ALL + '{}'.format(text) + Style.RESET_ALL, file=sys.stderr) 75 | elif warning: 76 | print(Fore.YELLOW + '[{}] '.format(timestamp) + Style.RESET_ALL + '{}'.format(text) + Style.RESET_ALL) 77 | else: 78 | print(Fore.GREEN + '[{}] '.format(timestamp) + Style.RESET_ALL + '{}'.format(text) + Style.RESET_ALL) 79 | timestamp_sd = strftime('%b %d %H:%M:%S', localtime()) 80 | if sd_notify: 81 | sd_notifier.notify('STATUS={} - {}.'.format(timestamp_sd, unidecode(text))) 82 | 83 | # convert device type to human-readable name 84 | def sensor_type_to_name(sensor_type): 85 | return sensor_name_miflora if (sensor_type == sensor_type_miflora ) else sensor_name_mitempbt 86 | 87 | # Identifier cleanup 88 | def clean_identifier(name): 89 | clean = name.strip() 90 | for this, that in [[' ', '-'], ['ä', 'ae'], ['Ä', 'Ae'], ['ö', 'oe'], ['Ö', 'Oe'], ['ü', 'ue'], ['Ü', 'Ue'], ['ß', 'ss']]: 91 | clean = clean.replace(this, that) 92 | clean = unidecode(clean) 93 | return clean 94 | 95 | # Eclipse Paho callbacks - http://www.eclipse.org/paho/clients/python/docs/#callbacks 96 | def on_connect(client, userdata, flags, rc): 97 | if rc == 0: 98 | print_line('MQTT connection established', console=True, sd_notify=True) 99 | print() 100 | else: 101 | print_line('Connection error with result code {} - {}'.format(str(rc), mqtt.connack_string(rc)), error=True) 102 | #kill main thread 103 | os._exit(1) 104 | 105 | 106 | def on_publish(client, userdata, mid): 107 | #print_line('Data successfully published.') 108 | pass 109 | 110 | 111 | def sensors_to_openhab_items(sensor_type, sensors, sensor_params, reporting_mode): 112 | sensor_type_name = sensor_type_to_name(sensor_type) 113 | print_line('Generating openHAB items. Copy to your configuration and modify as needed...') 114 | items = list() 115 | items.append('// {}.items - Generated by miflora-mqtt-daemon.'.format(sensor_type.lower())) 116 | items.append('// Adapt to your needs! Things you probably want to modify:') 117 | items.append('// Room group names, icons,') 118 | items.append('// "gAll", "broker", "UnknownRoom"') 119 | items.append('') 120 | items.append('// {} specific groups'.format(sensor_type_name)) 121 | items.append('Group g{} "All {} sensors and elements" (gAll)'.format(sensor_type, sensor_type_name)) 122 | for param, param_properties in sensor_params.items(): 123 | items.append('Group g{} "{} {} elements" (gAll, g{})'.format(param_properties['name'], sensor_type_name, param_properties['name_pretty'], sensor_type)) 124 | if reporting_mode == 'mqtt-json': 125 | for [sensor_name, sensor] in sensors.items(): 126 | location = sensor['location_clean'] if sensor['location_clean'] else 'UnknownRoom' 127 | items.append('\n// {} "{}" ({})'.format(sensor_type_name, sensor['name_pretty'], sensor['mac'])) 128 | items.append('Group g{}{} "{} Sensor {}" (g{}, g{})'.format(location, sensor_name, sensor_type_name, sensor['name_pretty'], sensor_type, location)) 129 | for [param, param_properties] in sensor_params.items(): 130 | basic = 'Number {}_{}_{}'.format(location, sensor_name, param_properties['name']) 131 | label = '"{} {} {} [{} {}]"'.format(location, sensor['name_pretty'], param_properties['name_pretty'], param_properties['typeformat'], param_properties['unit'].replace('%', '%%')) 132 | details = ' (g{}{}, g{})'.format(location, sensor_name, param_properties['name']) 133 | channel = '{{mqtt="<[broker:{}/{}:state:JSONPATH($.{})]"}}'.format(base_topic, sensor_name, param) 134 | items.append(' '.join([basic, label, details, channel])) 135 | items.append('') 136 | print('\n'.join(items)) 137 | #elif reporting_mode == 'mqtt-homie': 138 | else: 139 | raise IOError('Given reporting_mode not supported for the export to openHAB items') 140 | 141 | # Init sensors from configuration files 142 | def init_sensors(sensor_type, sensors): 143 | sensor_type_name = sensor_type_to_name(sensor_type) 144 | if sensor_type == sensor_type_miflora: 145 | config_section = sensor_type_miflora 146 | mac_regexp = "C4:7C:8D:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}" 147 | elif sensor_type == sensor_type_mitempbt: 148 | config_section = sensor_type_mitempbt 149 | mac_regexp = "(4C:65:A8|58:2D:34):[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}" 150 | else: 151 | print_line('Unknown device type: {}'.format(sensor_type), error=True, sd_notify=True) 152 | sys.exit(1) 153 | 154 | for [name, mac] in config[config_section].items(): 155 | if not re.match(mac_regexp, mac): 156 | print_line('The MAC address "{}" seems to be in the wrong format. Please check your configuration'.format(mac), error=True, sd_notify=True) 157 | sys.exit(1) 158 | 159 | if '@' in name: 160 | name_pretty, location_pretty = name.split('@') 161 | else: 162 | name_pretty, location_pretty = name, '' 163 | name_clean = clean_identifier(name_pretty) 164 | location_clean = clean_identifier(location_pretty) 165 | 166 | sensor = dict() 167 | print('Adding sensor to device list and testing connection ...') 168 | print('Name: "{}"'.format(name_pretty)) 169 | #print_line('Attempting initial connection to Mi Flora sensor "{}" ({})'.format(name_pretty, mac), console=False, sd_notify=True) 170 | 171 | if sensor_type == sensor_type_miflora: 172 | sensor_poller = MiFloraPoller(mac=mac, backend=BluepyBackend, cache_timeout=miflora_cache_timeout, retries=3, adapter=used_adapter) 173 | elif sensor_type == sensor_type_mitempbt: 174 | sensor_poller = MiThermometerPoller(mac=mac, backend=BluepyBackend, cache_timeout=mitempbt_cache_timeout, retries=3, adapter=used_adapter) 175 | 176 | sensor['poller'] = sensor_poller 177 | sensor['name_pretty'] = name_pretty 178 | sensor['mac'] = sensor_poller._mac 179 | sensor['refresh'] = miflora_sleep_period if (sensor_type == sensor_type_miflora) else mitempbt_sleep_period 180 | sensor['location_clean'] = location_clean 181 | sensor['location_pretty'] = location_pretty 182 | sensor['stats'] = {"count": 0, "success": 0, "failure": 0} 183 | try: 184 | sensor_poller.fill_cache() 185 | sensor_poller.parameter_value(MI_BATTERY) 186 | sensor['firmware'] = sensor_poller.firmware_version() 187 | except (IOError, BluetoothBackendException, BTLEException, RuntimeError, BrokenPipeError): 188 | print_line('Initial connection to {} sensor "{}" ({}) failed.'.format(sensor_type_name, name_pretty, mac), error=True, sd_notify=True) 189 | else: 190 | print('Internal name: "{}"'.format(name_clean)) 191 | print('Device name: "{}"'.format(sensor_poller.name())) 192 | print('MAC address: {}'.format(sensor_poller._mac)) 193 | print('Firmware: {}'.format(sensor_poller.firmware_version())) 194 | print_line('Initial connection to {} sensor "{}" ({}) successful'.format(sensor_type_name, name_pretty, mac), sd_notify=True) 195 | print() 196 | sensors[name_clean] = sensor 197 | 198 | # Pool & publish information from sensors 199 | def pool_sensors(sensor_type, sensors, parameters): 200 | sensor_type_name = sensor_type_to_name(sensor_type) 201 | for [sensor_name, sensor] in sensors.items(): 202 | data = dict() 203 | attempts = 2 204 | sensor['poller']._cache = None 205 | sensor['poller']._last_read = None 206 | sensor['stats']['count'] = sensor['stats']['count'] + 1 207 | print_line('Retrieving data from {} sensor "{}" ...'.format(sensor_type_name, sensor['name_pretty'])) 208 | while attempts != 0 and not sensor['poller']._cache: 209 | try: 210 | sensor['poller'].fill_cache() 211 | sensor['poller'].parameter_value(MI_BATTERY) 212 | except (IOError, BluetoothBackendException, BTLEException, RuntimeError, BrokenPipeError) as e: 213 | attempts = attempts - 1 214 | if attempts > 0: 215 | print_line('Retrying ...', warning = True) 216 | if len(str(e))>0: 217 | print_line('\tDue to: {}'.format(e), error=True) 218 | sensor['poller']._cache = None 219 | sensor['poller']._last_read = None 220 | 221 | if not sensor['poller']._cache: 222 | sensor['stats']['failure'] = sensor['stats']['failure'] + 1 223 | print_line('Failed to retrieve data from {} sensor "{}" ({}), success rate: {:.0%}'.format( 224 | sensor_type_name, sensor['name_pretty'], sensor['mac'], sensor['stats']['success']/sensor['stats']['count'] 225 | ), error = True, sd_notify = True) 226 | print() 227 | continue 228 | else: 229 | sensor['stats']['success'] = sensor['stats']['success'] + 1 230 | 231 | for param,_ in parameters.items(): 232 | data[param] = sensor['poller'].parameter_value(param) 233 | print_line('Result: {}'.format(json.dumps(data))) 234 | 235 | if reporting_mode == 'mqtt-json': 236 | print_line('Publishing to MQTT topic "{}/{}"'.format(base_topic, sensor_name)) 237 | mqtt_client.publish('{}/{}'.format(base_topic, sensor_name), json.dumps(data)) 238 | sleep(0.5) # some slack for the publish roundtrip and callback function 239 | elif reporting_mode == 'thingsboard-json': 240 | print_line('Publishing to MQTT topic "{}" username "{}"'.format(base_topic, sensor_name)) 241 | mqtt_client.username_pw_set(sensor_name) 242 | mqtt_client.reconnect() 243 | sleep(1.0) 244 | mqtt_client.publish('{}'.format(base_topic), json.dumps(data)) 245 | sleep(0.5) # some slack for the publish roundtrip and callback function 246 | elif reporting_mode == 'homeassistant-mqtt': 247 | print_line('Publishing to MQTT topic "{}/sensor/{}/state"'.format(base_topic, sensor_name).lower()) 248 | mqtt_client.publish('{}/sensor/{}/state'.format(base_topic, sensor_name).lower(), json.dumps(data), retain=True) 249 | sleep(0.5) # some slack for the publish roundtrip and callback function 250 | elif reporting_mode == 'mqtt-homie': 251 | print_line('Publishing data to MQTT base topic "{}/{}/{}"'.format(base_topic, device_id, sensor_name)) 252 | for [param, value] in data.items(): 253 | mqtt_client.publish('{}/{}/{}/{}'.format(base_topic, device_id, sensor_name, param), value, 1, retain=False) 254 | sleep(0.5) # some slack for the publish roundtrip and callback function 255 | elif reporting_mode == 'mqtt-smarthome': 256 | for [param, value] in data.items(): 257 | print_line('Publishing data to MQTT topic "{}/status/{}/{}"'.format(base_topic, sensor_name, param)) 258 | payload = dict() 259 | payload['val'] = value 260 | payload['ts'] = int(round(time() * 1000)) 261 | mqtt_client.publish('{}/status/{}/{}'.format(base_topic, sensor_name, param), json.dumps(payload), retain=True) 262 | sleep(0.5) # some slack for the publish roundtrip and callback function 263 | elif reporting_mode == 'wirenboard-mqtt': 264 | for [param, value] in data.items(): 265 | print_line('Publishing data to MQTT topic "/devices/{}/controls/{}"'.format(sensor_name, param)) 266 | mqtt_client.publish('/devices/{}/controls/{}'.format(sensor_name, param), value, retain=True) 267 | mqtt_client.publish('/devices/{}/controls/{}'.format(sensor_name, 'timestamp'), strftime('%Y-%m-%d %H:%M:%S', localtime()), retain=True) 268 | sleep(0.5) # some slack for the publish roundtrip and callback function 269 | elif reporting_mode == 'json': 270 | data['timestamp'] = strftime('%Y-%m-%d %H:%M:%S', localtime()) 271 | data['name'] = sensor_name 272 | data['name_pretty'] = sensor['name_pretty'] 273 | data['mac'] = sensor['mac'] 274 | data['firmware'] = sensor['firmware'] 275 | print('Data for "{}": {}'.format(sensor_name, json.dumps(data))) 276 | else: 277 | raise NameError('Unexpected reporting_mode.') 278 | print() 279 | 280 | class sensorPooler(threading.Thread): 281 | def __init__(self, sensor_type, sensors, sensor_parameters, sleep_period, hciLock): 282 | threading.Thread.__init__(self) 283 | self.sensor_type = sensor_type 284 | self.sensor_type_name = sensor_type_to_name(sensor_type) 285 | self.sensors = sensors 286 | self.sensor_parameters = sensor_parameters 287 | self.sleep_period = sleep_period 288 | self.hciLock = hciLock 289 | self.daemon = True 290 | self.start() 291 | 292 | def run(self): 293 | print_line('Worker for {} sensors started'.format(self.sensor_type_name), sd_notify=True) 294 | # Sensor data retrieving and publishing 295 | while True: 296 | with self.hciLock: 297 | pool_sensors(self.sensor_type, self.sensors, self.sensor_parameters) 298 | 299 | if daemon_enabled: 300 | print_line('Sleeping for {} ({} seconds) ...'.format(self.sensor_type_name, self.sleep_period)) 301 | print() 302 | sleep(self.sleep_period) 303 | else: 304 | break 305 | 306 | print_line('Execution finished for {}'.format(self.sensor_type_name), sd_notify=True) 307 | print() 308 | 309 | # Load configuration file 310 | config_dir = parse_args.config_dir 311 | 312 | config = ConfigParser(delimiters=('=', )) 313 | config.optionxform = str 314 | config.read([os.path.join(config_dir, 'config.ini.dist'), os.path.join(config_dir, 'config.ini')]) 315 | 316 | reporting_mode = config['General'].get('reporting_method', 'mqtt-json') 317 | used_adapter = config['General'].get('adapter', 'hci0') 318 | daemon_enabled = config['Daemon'].getboolean('enabled', True) 319 | 320 | if reporting_mode == 'mqtt-homie': 321 | default_base_topic = 'homie' 322 | elif reporting_mode == 'homeassistant-mqtt': 323 | default_base_topic = 'homeassistant' 324 | elif reporting_mode == 'thingsboard-json': 325 | default_base_topic = 'v1/devices/me/telemetry' 326 | elif reporting_mode == 'wirenboard-mqtt': 327 | default_base_topic = '' 328 | else: 329 | default_base_topic = 'misensor' 330 | 331 | base_topic = config['MQTT'].get('base_topic', default_base_topic).lower() 332 | device_id = config['MQTT'].get('homie_device_id', 'miflora-mqtt-daemon').lower() 333 | miflora_sleep_period = config['Daemon'].getint('period_miflora', 300) 334 | miflora_cache_timeout = miflora_sleep_period - 1 335 | mitempbt_sleep_period = config['Daemon'].getint('period_mitempbt', 60) 336 | mitempbt_cache_timeout = mitempbt_sleep_period - 1 337 | 338 | # Check configuration 339 | if reporting_mode not in ['mqtt-json', 'mqtt-homie', 'json', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json', 'wirenboard-mqtt']: 340 | print_line('Configuration parameter reporting_mode set to an invalid value', error=True, sd_notify=True) 341 | sys.exit(1) 342 | if not config[sensor_type_miflora] and not config[sensor_type_mitempbt]: 343 | print_line('No sensors found in configuration file "config.ini"', error=True, sd_notify=True) 344 | sys.exit(1) 345 | if reporting_mode == 'wirenboard-mqtt' and base_topic: 346 | print_line('Parameter "base_topic" ignored for "reporting_method = wirenboard-mqtt"', warning=True, sd_notify=True) 347 | 348 | 349 | print_line('Configuration accepted', console=False, sd_notify=True) 350 | 351 | # MQTT connection 352 | if reporting_mode in ['mqtt-json', 'mqtt-homie', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json', 'wirenboard-mqtt']: 353 | print_line('Connecting to MQTT broker ...') 354 | mqtt_client = mqtt.Client() 355 | mqtt_client.on_connect = on_connect 356 | mqtt_client.on_publish = on_publish 357 | if reporting_mode == 'mqtt-json': 358 | mqtt_client.will_set('{}/$announce'.format(base_topic), payload='{}', retain=True) 359 | elif reporting_mode == 'mqtt-homie': 360 | mqtt_client.will_set('{}/{}/$online'.format(base_topic, device_id), payload='false', retain=True) 361 | elif reporting_mode == 'mqtt-smarthome': 362 | mqtt_client.will_set('{}/connected'.format(base_topic), payload='0', retain=True) 363 | 364 | if config['MQTT'].getboolean('tls', False): 365 | # According to the docs, setting PROTOCOL_SSLv23 "Selects the highest protocol version 366 | # that both the client and server support. Despite the name, this option can select 367 | # “TLS” protocols as well as “SSL”" - so this seems like a resonable default 368 | mqtt_client.tls_set( 369 | ca_certs=config['MQTT'].get('tls_ca_cert', None), 370 | keyfile=config['MQTT'].get('tls_keyfile', None), 371 | certfile=config['MQTT'].get('tls_certfile', None), 372 | tls_version=ssl.PROTOCOL_SSLv23 373 | ) 374 | 375 | if config['MQTT'].get('username'): 376 | mqtt_client.username_pw_set(config['MQTT'].get('username'), config['MQTT'].get('password', None)) 377 | try: 378 | mqtt_client.connect(config['MQTT'].get('hostname', 'localhost'), 379 | port=config['MQTT'].getint('port', 1883), 380 | keepalive=config['MQTT'].getint('keepalive', 60)) 381 | except: 382 | print_line('MQTT connection error. Please check your settings in the configuration file "config.ini"', error=True, sd_notify=True) 383 | sys.exit(1) 384 | else: 385 | if reporting_mode == 'mqtt-smarthome': 386 | mqtt_client.publish('{}/connected'.format(base_topic), payload='1', retain=True) 387 | if reporting_mode != 'thingsboard-json': 388 | mqtt_client.loop_start() 389 | sleep(1.0) # some slack to establish the connection 390 | 391 | sd_notifier.notify('READY=1') 392 | 393 | # Initialize Mi sensors 394 | mifloras = OrderedDict() 395 | init_sensors(sensor_type_miflora, mifloras) 396 | mitempbts = OrderedDict() 397 | init_sensors(sensor_type_mitempbt, mitempbts) 398 | 399 | # openHAB items generation 400 | if parse_args.gen_openhab: 401 | sensors_to_openhab_items(sensor_type_miflora, mifloras, miflora_parameters, reporting_mode) 402 | sensors_to_openhab_items(sensor_type_mitempbt, mitempbts, mitempbt_parameters, reporting_mode) 403 | sys.exit(0) 404 | 405 | # Discovery Announcement 406 | if reporting_mode == 'mqtt-json': 407 | print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_name_miflora,sensor_name_mitempbt)) 408 | sensors_info = dict() 409 | for [sensor_name, sensor] in chain(mifloras.items(),mitempbts.items()): 410 | sensor_info = {key: value for key, value in sensor.items() if key not in ['poller', 'stats']} 411 | sensor_info['topic'] = '{}/{}'.format(base_topic, sensor_name) 412 | sensors_info[sensor_name] = sensor_info 413 | mqtt_client.publish('{}/$announce'.format(base_topic), json.dumps(sensors_info), retain=True) 414 | sleep(0.5) # some slack for the publish roundtrip and callback function 415 | print() 416 | elif reporting_mode == 'mqtt-homie': 417 | print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_name_miflora,sensor_name_mitempbt)) 418 | mqtt_client.publish('{}/{}/$homie'.format(base_topic, device_id), '2.1.0-alpha', 1, True) 419 | mqtt_client.publish('{}/{}/$online'.format(base_topic, device_id), 'true', 1, True) 420 | mqtt_client.publish('{}/{}/$name'.format(base_topic, device_id), device_id, 1, True) 421 | #mqtt_client.publish('{}/{}/$fw/version'.format(base_topic, device_id), flora['firmware'], 1, True) 422 | 423 | nodes_list = ','.join([sensor_name for [sensor_name, sensor] in chain(mifloras.items(), mitempbts.items())]) 424 | mqtt_client.publish('{}/{}/$nodes'.format(base_topic, device_id), nodes_list, 1, True) 425 | 426 | for [sensor_name, sensor] in mifloras.items(): 427 | topic_path = '{}/{}/{}'.format(base_topic, device_id, sensor_name) 428 | mqtt_client.publish('{}/$name'.format(topic_path), sensor['name_pretty'], 1, True) 429 | mqtt_client.publish('{}/$type'.format(topic_path), 'miflora', 1, True) 430 | mqtt_client.publish('{}/$properties'.format(topic_path), 'battery,conductivity,light,moisture,temperature', 1, True) 431 | mqtt_client.publish('{}/battery/$settable'.format(topic_path), 'false', 1, True) 432 | mqtt_client.publish('{}/battery/$unit'.format(topic_path), 'percent', 1, True) 433 | mqtt_client.publish('{}/battery/$datatype'.format(topic_path), 'int', 1, True) 434 | mqtt_client.publish('{}/battery/$range'.format(topic_path), '0:100', 1, True) 435 | mqtt_client.publish('{}/conductivity/$settable'.format(topic_path), 'false', 1, True) 436 | mqtt_client.publish('{}/conductivity/$unit'.format(topic_path), 'µS/cm', 1, True) 437 | mqtt_client.publish('{}/conductivity/$datatype'.format(topic_path), 'int', 1, True) 438 | mqtt_client.publish('{}/conductivity/$range'.format(topic_path), '0:*', 1, True) 439 | mqtt_client.publish('{}/light/$settable'.format(topic_path), 'false', 1, True) 440 | mqtt_client.publish('{}/light/$unit'.format(topic_path), 'lux', 1, True) 441 | mqtt_client.publish('{}/light/$datatype'.format(topic_path), 'int', 1, True) 442 | mqtt_client.publish('{}/light/$range'.format(topic_path), '0:50000', 1, True) 443 | mqtt_client.publish('{}/moisture/$settable'.format(topic_path), 'false', 1, True) 444 | mqtt_client.publish('{}/moisture/$unit'.format(topic_path), 'percent', 1, True) 445 | mqtt_client.publish('{}/moisture/$datatype'.format(topic_path), 'int', 1, True) 446 | mqtt_client.publish('{}/moisture/$range'.format(topic_path), '0:100', 1, True) 447 | mqtt_client.publish('{}/temperature/$settable'.format(topic_path), 'false', 1, True) 448 | mqtt_client.publish('{}/temperature/$unit'.format(topic_path), '°C', 1, True) 449 | mqtt_client.publish('{}/temperature/$datatype'.format(topic_path), 'float', 1, True) 450 | mqtt_client.publish('{}/temperature/$range'.format(topic_path), '*', 1, True) 451 | 452 | for [sensor_name, sensor] in mitempbts.items(): 453 | topic_path = '{}/{}/{}'.format(base_topic, device_id, sensor_name) 454 | mqtt_client.publish('{}/$name'.format(topic_path), sensor['name_pretty'], 1, True) 455 | mqtt_client.publish('{}/$type'.format(topic_path), 'mitempbt', 1, True) 456 | mqtt_client.publish('{}/$properties'.format(topic_path), 'battery,humidity,temperature', 1, True) 457 | mqtt_client.publish('{}/battery/$settable'.format(topic_path), 'false', 1, True) 458 | mqtt_client.publish('{}/battery/$unit'.format(topic_path), 'percent', 1, True) 459 | mqtt_client.publish('{}/battery/$datatype'.format(topic_path), 'int', 1, True) 460 | mqtt_client.publish('{}/battery/$range'.format(topic_path), '0:100', 1, True) 461 | mqtt_client.publish('{}/humidity/$settable'.format(topic_path), 'false', 1, True) 462 | mqtt_client.publish('{}/humidity/$unit'.format(topic_path), 'percent', 1, True) 463 | mqtt_client.publish('{}/humidity/$datatype'.format(topic_path), 'int', 1, True) 464 | mqtt_client.publish('{}/humidity/$range'.format(topic_path), '0:100', 1, True) 465 | mqtt_client.publish('{}/temperature/$settable'.format(topic_path), 'false', 1, True) 466 | mqtt_client.publish('{}/temperature/$unit'.format(topic_path), '°C', 1, True) 467 | mqtt_client.publish('{}/temperature/$datatype'.format(topic_path), 'float', 1, True) 468 | mqtt_client.publish('{}/temperature/$range'.format(topic_path), '*', 1, True) 469 | sleep(0.5) # some slack for the publish roundtrip and callback function 470 | print() 471 | elif reporting_mode == 'homeassistant-mqtt': 472 | print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_name_miflora,sensor_name_mitempbt)) 473 | for [flora_name, flora] in mifloras.items(): 474 | topic_path = '{}/sensor/{}'.format(base_topic, flora_name) 475 | base_payload = { 476 | "state_topic": "{}/state".format(topic_path).lower() 477 | } 478 | for sensor, params in miflora_parameters.items(): 479 | payload = dict(base_payload.items()) 480 | payload['unit_of_measurement'] = params['unit'] 481 | payload['value_template'] = "{{ value_json.%s }}" % (sensor, ) 482 | payload['name'] = "{} {}".format(flora_name, sensor.title()) 483 | if 'device_class' in params: 484 | payload['device_class'] = params['device_class'] 485 | mqtt_client.publish('{}/{}_{}/config'.format(topic_path, flora_name, sensor).lower(), json.dumps(payload), 1, True) 486 | for [mitempbt_name, mitempbt] in mitempbts.items(): 487 | topic_path = '{}/sensor/{}'.format(base_topic, mitempbt_name) 488 | base_payload = { 489 | "state_topic": "{}/state".format(topic_path).lower() 490 | } 491 | for sensor, params in mitempbt_parameters.items(): 492 | payload = dict(base_payload.items()) 493 | payload['unit_of_measurement'] = params['unit'] 494 | payload['value_template'] = "{{ value_json.%s }}" % (sensor, ) 495 | payload['name'] = "{} {}".format(mitempbt_name, sensor.title()) 496 | if 'device_class' in params: 497 | payload['device_class'] = params['device_class'] 498 | mqtt_client.publish('{}/{}_{}/config'.format(topic_path, mitempbt_name, sensor).lower(), json.dumps(payload), 1, True) 499 | elif reporting_mode == 'wirenboard-mqtt': 500 | print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_name_miflora, sensor_name_mitempbt)) 501 | for [flora_name, flora] in mifloras.items(): 502 | mqtt_client.publish('/devices/{}/meta/name'.format(flora_name), flora_name, 1, True) 503 | topic_path = '/devices/{}/controls'.format(flora_name) 504 | mqtt_client.publish('{}/battery/meta/type'.format(topic_path), 'value', 1, True) 505 | mqtt_client.publish('{}/battery/meta/units'.format(topic_path), '%', 1, True) 506 | mqtt_client.publish('{}/conductivity/meta/type'.format(topic_path), 'value', 1, True) 507 | mqtt_client.publish('{}/conductivity/meta/units'.format(topic_path), 'µS/cm', 1, True) 508 | mqtt_client.publish('{}/light/meta/type'.format(topic_path), 'value', 1, True) 509 | mqtt_client.publish('{}/light/meta/units'.format(topic_path), 'lux', 1, True) 510 | mqtt_client.publish('{}/moisture/meta/type'.format(topic_path), 'rel_humidity', 1, True) 511 | mqtt_client.publish('{}/temperature/meta/type'.format(topic_path), 'temperature', 1, True) 512 | mqtt_client.publish('{}/timestamp/meta/type'.format(topic_path), 'text', 1, True) 513 | sleep(0.5) # some slack for the publish roundtrip and callback function 514 | 515 | for [mitempbt_name, mitempbt] in mitempbts.items(): 516 | mqtt_client.publish('/devices/{}/meta/name'.format(mitempbt_name), mitempbt_name, 1, True) 517 | topic_path = '/devices/{}/controls'.format(mitempbt_name) 518 | mqtt_client.publish('{}/battery/meta/type'.format(topic_path), 'value', 1, True) 519 | mqtt_client.publish('{}/battery/meta/units'.format(topic_path), '%', 1, True) 520 | mqtt_client.publish('{}/humidity/meta/type'.format(topic_path), 'rel_humidity', 1, True) 521 | mqtt_client.publish('{}/temperature/meta/type'.format(topic_path), 'temperature', 1, True) 522 | mqtt_client.publish('{}/timestamp/meta/type'.format(topic_path), 'text', 1, True) 523 | sleep(0.5) # some slack for the publish roundtrip and callback function 524 | print() 525 | 526 | print_line('Initialization complete, starting MQTT publish loop', console=False, sd_notify=True) 527 | 528 | hciLock = threading.Lock() 529 | threads = [] 530 | 531 | if len(mifloras) != 0: 532 | threads.append(sensorPooler(sensor_type_miflora, mifloras, miflora_parameters, miflora_sleep_period, hciLock)) 533 | 534 | if len(mitempbts) != 0: 535 | threads.append(sensorPooler(sensor_type_mitempbt, mitempbts, mitempbt_parameters, mitempbt_sleep_period, hciLock)) 536 | 537 | for thread in threads: 538 | thread.join() 539 | 540 | print ("Exiting Main Thread") 541 | if reporting_mode == 'mqtt-json': 542 | mqtt_client.disconnect() 543 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | #git+https://github.com/open-homeautomation/miflora.git#egg=miflora 2 | miflora==0.4 3 | paho-mqtt==1.4.0 4 | wheel==0.29.0 5 | sdnotify==0.3.1 6 | colorama==0.3.9 7 | Unidecode==0.4.21 8 | bluepy==1.3.0 9 | btlewrap==0.0.3 10 | mithermometer==0.1.2 -------------------------------------------------------------------------------- /template.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon 3 | Documentation=https://github.com/ThomDietrich/miflora-mqtt-daemon 4 | After=bluetooth.service mosquitto.service 5 | 6 | [Service] 7 | Type=notify 8 | User=daemon 9 | Group=daemon 10 | WorkingDirectory=/opt/miflora-mqtt-daemon/ 11 | ExecStart=/opt/miflora-mqtt-daemon/miflora-mqtt-daemon.py 12 | StandardOutput=null 13 | StandardError=journal 14 | Environment=PYTHONUNBUFFERED=true 15 | Restart=always 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | --------------------------------------------------------------------------------