From affeac7b80f6b7e61c9886636677edcf6a5f35e9 Mon Sep 17 00:00:00 2001 From: aqualx Date: Mon, 14 Jan 2019 00:40:02 +0200 Subject: [PATCH 01/15] Added support for Wiren Board MQTT Conventions --- config.ini.dist | 3 +++ miflora-mqtt-daemon.py | 28 ++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/config.ini.dist b/config.ini.dist index 506fd3a..cd282b0 100644 --- a/config.ini.dist +++ b/config.ini.dist @@ -17,6 +17,8 @@ # (https://www.home-assistant.io/docs/mqtt/discovery/) # thingsboard-json - Publish to the ThingsBoard MQTT broker # (https://thingsboard.io) +# wirenboard-mqtt - Publish to the Wiren Board MQTT broker +# (https://wirenboard.com) # json - Print to stdout as json encoded strings # #reporting_method = mqtt-json @@ -49,6 +51,7 @@ #base_topic = homie # Default for: mqtt-homie #base_topic = homeassistant # Default for: homeassistant-mqtt #base_topic = v1/devices/me/telemetry # Default for: thingsboard-json +#base_topic = # Default for: wirenboard-mqtt # Homie specific: The device ID for this daemon instance (Default: miflora-mqtt-daemon) #homie_device_id = miflora-mqtt-daemon diff --git a/miflora-mqtt-daemon.py b/miflora-mqtt-daemon.py index 075f757..1b524e0 100755 --- a/miflora-mqtt-daemon.py +++ b/miflora-mqtt-daemon.py @@ -133,6 +133,8 @@ def flores_to_openhab_items(flores, reporting_mode): default_base_topic = 'homeassistant' elif reporting_mode == 'thingsboard-json': default_base_topic = 'v1/devices/me/telemetry' +elif reporting_mode == 'wirenboard-mqtt': + default_base_topic = '' else: default_base_topic = 'miflora' @@ -142,7 +144,7 @@ def flores_to_openhab_items(flores, reporting_mode): miflora_cache_timeout = sleep_period - 1 # Check configuration -if reporting_mode not in ['mqtt-json', 'mqtt-homie', 'json', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json']: +if reporting_mode not in ['mqtt-json', 'mqtt-homie', 'json', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json', 'wirenboard-mqtt']: print_line('Configuration parameter reporting_mode set to an invalid value', error=True, sd_notify=True) sys.exit(1) if not config['Sensors']: @@ -152,7 +154,7 @@ def flores_to_openhab_items(flores, reporting_mode): print_line('Configuration accepted', console=False, sd_notify=True) # MQTT connection -if reporting_mode in ['mqtt-json', 'mqtt-homie', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json']: +if reporting_mode in ['mqtt-json', 'mqtt-homie', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json', 'wirenboard-mqtt']: print_line('Connecting to MQTT broker ...') mqtt_client = mqtt.Client() mqtt_client.on_connect = on_connect @@ -303,6 +305,22 @@ def flores_to_openhab_items(flores, reporting_mode): if 'device_class' in params: payload['device_class'] = params['device_class'] mqtt_client.publish('{}/{}_{}/config'.format(topic_path, flora_name, sensor).lower(), json.dumps(payload), 1, True) +elif reporting_mode == 'wirenboard-mqtt': + print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...') + for [flora_name, flora] in flores.items(): + mqtt_client.publish('/devices/{}/meta/name'.format(flora_name), flora_name, 1, True) + topic_path = '/devices/{}/controls'.format(flora_name) + mqtt_client.publish('{}/battery/meta/type'.format(topic_path), 'value', 1, True) + mqtt_client.publish('{}/battery/meta/units'.format(topic_path), '%', 1, True) + mqtt_client.publish('{}/conductivity/meta/type'.format(topic_path), 'value', 1, True) + mqtt_client.publish('{}/conductivity/meta/units'.format(topic_path), 'µS/cm', 1, True) + mqtt_client.publish('{}/light/meta/type'.format(topic_path), 'value', 1, True) + mqtt_client.publish('{}/light/meta/units'.format(topic_path), 'lux', 1, True) + mqtt_client.publish('{}/moisture/meta/type'.format(topic_path), 'rel_humidity', 1, True) + mqtt_client.publish('{}/temperature/meta/type'.format(topic_path), 'temperature', 1, True) + mqtt_client.publish('{}/timestamp/meta/type'.format(topic_path), 'text', 1, True) + sleep(0.5) # some slack for the publish roundtrip and callback function + print() print_line('Initialization complete, starting MQTT publish loop', console=False, sd_notify=True) @@ -369,6 +387,12 @@ def flores_to_openhab_items(flores, reporting_mode): payload['ts'] = int(round(time() * 1000)) mqtt_client.publish('{}/status/{}/{}'.format(base_topic, flora_name, param), json.dumps(payload), retain=True) sleep(0.5) # some slack for the publish roundtrip and callback function + elif reporting_mode == 'wirenboard-mqtt': + for [param, value] in data.items(): + print_line('Publishing data to MQTT topic "/devices/{}/controls/{}"'.format(flora_name, param)) + mqtt_client.publish('/devices/{}/controls/{}'.format(flora_name, param), value, retain=True) + mqtt_client.publish('/devices/{}/controls/{}'.format(flora_name, 'timestamp'), strftime('%Y-%m-%d %H:%M:%S', localtime()), retain=True) + sleep(0.5) # some slack for the publish roundtrip and callback function elif reporting_mode == 'json': data['timestamp'] = strftime('%Y-%m-%d %H:%M:%S', localtime()) data['name'] = flora_name From d60071f7aa5c7b541a75eeedbd6ff7dfe64a50bd Mon Sep 17 00:00:00 2001 From: aqualx Date: Tue, 15 Jan 2019 13:51:58 +0200 Subject: [PATCH 02/15] Added warning when reporting_mode set to 'wirenboard-mqtt' and base_topic defined in configuration file. --- miflora-mqtt-daemon.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/miflora-mqtt-daemon.py b/miflora-mqtt-daemon.py index 1b524e0..aa9be71 100755 --- a/miflora-mqtt-daemon.py +++ b/miflora-mqtt-daemon.py @@ -150,6 +150,9 @@ def flores_to_openhab_items(flores, reporting_mode): if not config['Sensors']: print_line('No sensors found in configuration file "config.ini"', error=True, sd_notify=True) sys.exit(1) +if reporting_mode == 'wirenboard-mqtt' and base_topic: + print_line('Parameter "base_topic" ignored for "reporting_method = wirenboard-mqtt"', warning=True, sd_notify=True) + print_line('Configuration accepted', console=False, sd_notify=True) From 1e1a7424fd2ac92030f6e3c9a0539d23c0dd8eb2 Mon Sep 17 00:00:00 2001 From: Oleksandr Kurbatov <45320692+aqualx@users.noreply.github.com> Date: Wed, 16 Jan 2019 18:39:43 +0200 Subject: [PATCH 03/15] Update README.md --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 231c456..3030632 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The program can be executed in **daemon mode** to run continuously in the backgr * following the [mqtt-smarthome architecture proposal](https://github.com/mqtt-smarthome/mqtt-smarthome) * using the [HomeAssistant MQTT discovery format](https://home-assistant.io/docs/mqtt/discovery/) * using the [ThingsBoard.io](https://thingsboard.io/) MQTT interface + * following the [Wiren Board MQTT Conventions](https://github.com/contactless/homeui/blob/master/conventions.md) * Announcement messages to support auto-discovery services * MQTT authentication support * No special/root privileges needed @@ -225,6 +226,15 @@ to integrate with [ThingsBoard.io](https://thingsboard.io/): 1. in your `config.ini` assign unique sensor names for your plants 1. on the ThingsBoard platform create devices and use `Access token` as `Credential type` and the chosen sensor name as token +### Wiren Board + +to integrate with [Wiren Board](https://wirenboard.com/en/) in your `config.ini` set: + +1. `reporting_method = wirenboard-mqtt` +1. set `hostname` with address of [Wiren Board](https://wirenboard.com/en/) controller and optionally `username` and `password` + +sensors will automatically appear on [Wiren Board](https://wirenboard.com/en/) as separate devices + ---- #### Disclaimer and Legal From ddb1b8b5eb9836d6b11bf07bdb0358a707fff8c9 Mon Sep 17 00:00:00 2001 From: aqualx Date: Mon, 28 Jan 2019 23:44:58 +0200 Subject: [PATCH 04/15] Added support for 'Xiaomi Mijia Smart Temperature and Humidity Sensor' --- config.ini.dist | 5 +- miflora-mqtt-daemon.py | 388 ++++++++++++++++++++++++----------------- 2 files changed, 235 insertions(+), 158 deletions(-) diff --git a/config.ini.dist b/config.ini.dist index cd282b0..36ee461 100644 --- a/config.ini.dist +++ b/config.ini.dist @@ -72,7 +72,7 @@ # Path to TLS client auth certificate file #tls_certfile = -[Sensors] +[MiFlora] # Add your Mi Flora sensors here. Each sensor consists of a name and a Ethernet MAC address. # Additional location information can be added to the name, delimited by an '@'. @@ -84,3 +84,6 @@ #Schefflera@Living = C4:7C:8D:11:22:33 #JapaneseBonsai = C4:7C:8D:44:55:66 #Petunia@Balcony = C4:7C:8D:77:88:99 + +[MiTempBt] +# Add your Mi Temerature & Humidity sensors here. Setup is same as for [MiFlora] section \ No newline at end of file diff --git a/miflora-mqtt-daemon.py b/miflora-mqtt-daemon.py index aa9be71..dfc3f8b 100755 --- a/miflora-mqtt-daemon.py +++ b/miflora-mqtt-daemon.py @@ -6,6 +6,7 @@ import json import os.path import argparse +from itertools import chain from time import time, sleep, localtime, strftime from collections import OrderedDict from colorama import init as colorama_init @@ -14,13 +15,16 @@ from unidecode import unidecode from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE from btlewrap import available_backends, BluepyBackend, GatttoolBackend, PygattBackend, BluetoothBackendException +from mitemp_bt.mitemp_bt_poller import MiTempBtPoller, MI_HUMIDITY import paho.mqtt.client as mqtt import sdnotify project_name = 'Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon' project_url = 'https://github.com/ThomDietrich/miflora-mqtt-daemon' -parameters = OrderedDict([ +sensor_type_miflora = "Mi Flora" +sensor_type_mitempbt = "Mi Temp & Humidity" +miflora_parameters = OrderedDict([ (MI_LIGHT, dict(name="LightIntensity", name_pretty='Sunlight Intensity', typeformat='%d', unit='lux', device_class="illuminance")), (MI_TEMPERATURE, dict(name="AirTemperature", name_pretty='Air Temperature', typeformat='%.1f', unit='°C', device_class="temperature")), (MI_MOISTURE, dict(name="SoilMoisture", name_pretty='Soil Moisture', typeformat='%d', unit='%', device_class="humidity")), @@ -28,9 +32,15 @@ (MI_BATTERY, dict(name="Battery", name_pretty='Sensor Battery Level', typeformat='%d', unit='%', device_class="battery")) ]) +mitempbt_parameters = OrderedDict([ + (MI_TEMPERATURE, dict(name="AirTemperature", name_pretty='Air Temperature', typeformat='%.1f', unit='°C', device_class="temperature")), + (MI_HUMIDITY, dict(name="Humidity", name_pretty='Air Moisture', typeformat='%d', unit='%', device_class="humidity")), + (MI_BATTERY, dict(name="Battery", name_pretty='Sensor Battery Level', typeformat='%d', unit='%', device_class="battery")) +]) + if False: # will be caught by python 2.7 to be illegal syntax - print('Sorry, this script requires a python3 runtime environemt.', file=sys.stderr) + print('Sorry, this script requires a python3 runtime environment.', file=sys.stderr) # Argparse parser = argparse.ArgumentParser(description=project_name, epilog='For further details see: ' + project_url) @@ -86,7 +96,7 @@ def on_publish(client, userdata, mid): pass -def flores_to_openhab_items(flores, reporting_mode): +def sensors_to_openhab_items(type, sensors, sensor_params, reporting_mode): print_line('Generating openHAB items. Copy to your configuration and modify as needed...') items = list() items.append('// miflora.items - Generated by miflora-mqtt-daemon.') @@ -94,20 +104,20 @@ def flores_to_openhab_items(flores, reporting_mode): items.append('// Room group names, icons,') items.append('// "gAll", "broker", "UnknownRoom"') items.append('') - items.append('// Mi Flora specific groups') - items.append('Group gMiFlora "All Mi Flora sensors and elements" (gAll)') - for param, param_properties in parameters.items(): - items.append('Group g{} "Mi Flora {} elements" (gAll, gMiFlora)'.format(param_properties['name'], param_properties['name_pretty'])) + items.append('// {} specific groups').format(type) + items.append('Group gMiFlora "All {} sensors and elements" (gAll)'.format(type)) + for param, param_properties in sensor_params.items(): + items.append('Group g{} "{} {} elements" (gAll, gMiFlora)'.format(param_properties['name'], type, param_properties['name_pretty'])) if reporting_mode == 'mqtt-json': - for [flora_name, flora] in flores.items(): - location = flora['location_clean'] if flora['location_clean'] else 'UnknownRoom' - items.append('\n// Mi Flora "{}" ({})'.format(flora['name_pretty'], flora['mac'])) - items.append('Group g{}{} "Mi Flora Sensor {}" (gMiFlora, g{})'.format(location, flora_name, flora['name_pretty'], location)) - for [param, param_properties] in parameters.items(): - basic = 'Number {}_{}_{}'.format(location, flora_name, param_properties['name']) - label = '"{} {} {} [{} {}]"'.format(location, flora['name_pretty'], param_properties['name_pretty'], param_properties['typeformat'], param_properties['unit'].replace('%', '%%')) - details = ' (g{}{}, g{})'.format(location, flora_name, param_properties['name']) - channel = '{{mqtt="<[broker:{}/{}:state:JSONPATH($.{})]"}}'.format(base_topic, flora_name, param) + for [sensor_name, sensor] in sensors.items(): + location = sensor['location_clean'] if sensor['location_clean'] else 'UnknownRoom' + items.append('\n// {} "{}" ({})'.format(type, flora['name_pretty'], flora['mac'])) + items.append('Group g{}{} "{} Sensor {}" (gMiFlora, g{})'.format(location, flora_name, type, sensor['name_pretty'], location)) + for [param, param_properties] in sensor_params.items(): + basic = 'Number {}_{}_{}'.format(location, sensor_name, param_properties['name']) + label = '"{} {} {} [{} {}]"'.format(location, sensor['name_pretty'], param_properties['name_pretty'], param_properties['typeformat'], param_properties['unit'].replace('%', '%%')) + details = ' (g{}{}, g{})'.format(location, sensor_name, param_properties['name']) + channel = '{{mqtt="<[broker:{}/{}:state:JSONPATH($.{})]"}}'.format(base_topic, sensor_name, param) items.append(' '.join([basic, label, details, channel])) items.append('') print('\n'.join(items)) @@ -115,6 +125,140 @@ def flores_to_openhab_items(flores, reporting_mode): else: raise IOError('Given reporting_mode not supported for the export to openHAB items') +# Init sensors from configuration files +def init_sensors(type, sensors): + if type == sensor_type_miflora: + config_section = "MiFlora" + mac_regexp = "C4:7C:8D:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}" + elif type == sensor_type_mitempbt: + config_section = "MiTempBt" + mac_regexp = "4C:65:A8:DB:[0-9A-F]{2}:[0-9A-F]{2}" + else: + print_line('Unknown device type: {}'.format(type), error=True, sd_notify=True) + sys.exit(1) + + for [name, mac] in config[config_section].items(): + if not re.match(mac_regexp, mac): + print_line('The MAC address "{}" seems to be in the wrong format. Please check your configuration'.format(mac), error=True, sd_notify=True) + sys.exit(1) + + if '@' in name: + name_pretty, location_pretty = name.split('@') + else: + name_pretty, location_pretty = name, '' + name_clean = clean_identifier(name_pretty) + location_clean = clean_identifier(location_pretty) + + sensor = dict() + print('Adding sensor to device list and testing connection ...') + print('Name: "{}"'.format(name_pretty)) + #print_line('Attempting initial connection to Mi Flora sensor "{}" ({})'.format(name_pretty, mac), console=False, sd_notify=True) + + if type == sensor_type_miflora: + sensor_poller = MiFloraPoller(mac=mac, backend=GatttoolBackend, cache_timeout=sensor_cache_timeout, retries=3, adapter=used_adapter) + elif type == sensor_type_mitempbt: + sensor_poller = MiTempBtPoller(mac=mac, backend=BluepyBackend, cache_timeout=sensor_cache_timeout, retries=3, adapter=used_adapter) + + sensor['poller'] = sensor_poller + sensor['name_pretty'] = name_pretty + sensor['mac'] = sensor_poller._mac + sensor['refresh'] = sleep_period + sensor['location_clean'] = location_clean + sensor['location_pretty'] = location_pretty + sensor['stats'] = {"count": 0, "success": 0, "failure": 0} + try: + sensor_poller.fill_cache() + sensor_poller.parameter_value(MI_BATTERY) + sensor['firmware'] = sensor_poller.firmware_version() + except (IOError, BluetoothBackendException): + print_line('Initial connection to {} sensor "{}" ({}) failed.'.format(type, name_pretty, mac), error=True, sd_notify=True) + else: + print('Internal name: "{}"'.format(name_clean)) + print('Device name: "{}"'.format(sensor_poller.name())) + print('MAC address: {}'.format(sensor_poller._mac)) + print('Firmware: {}'.format(sensor_poller.firmware_version())) + print_line('Initial connection to {} sensor "{}" ({}) successful'.format(type, name_pretty, mac), sd_notify=True) + print() + sensors[name_clean] = sensor + +# Pool & publish information from sensors +def pool_sensors(type, sensors, parameters): + for [sensor_name, sensor] in sensors.items(): + data = dict() + attempts = 2 + sensor['poller']._cache = None + sensor['poller']._last_read = None + sensor['stats']['count'] = sensor['stats']['count'] + 1 + print_line('Retrieving data from {} sensor "{}" ...'.format(type, sensor['name_pretty'])) + while attempts != 0 and not sensor['poller']._cache: + try: + sensor['poller'].fill_cache() + sensor['poller'].parameter_value(MI_BATTERY) + except (IOError, BluetoothBackendException): + attempts = attempts - 1 + if attempts > 0: + print_line('Retrying ...', warning = True) + sensor['poller']._cache = None + sensor['poller']._last_read = None + + if not sensor['poller']._cache: + sensor['stats']['failure'] = sensor['stats']['failure'] + 1 + print_line('Failed to retrieve data from {} sensor "{}" ({}), success rate: {:.0%}'.format( + type, sensor['name_pretty'], sensor['mac'], sensor['stats']['success']/sensor['stats']['count'] + ), error = True, sd_notify = True) + print() + continue + else: + sensor['stats']['success'] = sensor['stats']['success'] + 1 + + for param,_ in parameters.items(): + data[param] = sensor['poller'].parameter_value(param) + print_line('Result: {}'.format(json.dumps(data))) + + if reporting_mode == 'mqtt-json': + print_line('Publishing to MQTT topic "{}/{}"'.format(base_topic, sensor_name)) + mqtt_client.publish('{}/{}'.format(base_topic, sensor_name), json.dumps(data)) + sleep(0.5) # some slack for the publish roundtrip and callback function + elif reporting_mode == 'thingsboard-json': + print_line('Publishing to MQTT topic "{}" username "{}"'.format(base_topic, sensor_name)) + mqtt_client.username_pw_set(sensor_name) + mqtt_client.reconnect() + sleep(1.0) + mqtt_client.publish('{}'.format(base_topic), json.dumps(data)) + sleep(0.5) # some slack for the publish roundtrip and callback function + elif reporting_mode == 'homeassistant-mqtt': + print_line('Publishing to MQTT topic "{}/sensor/{}/state"'.format(base_topic, sensor_name).lower()) + mqtt_client.publish('{}/sensor/{}/state'.format(base_topic, sensor_name).lower(), json.dumps(data)) + sleep(0.5) # some slack for the publish roundtrip and callback function + elif reporting_mode == 'mqtt-homie': + print_line('Publishing data to MQTT base topic "{}/{}/{}"'.format(base_topic, device_id, sensor_name)) + for [param, value] in data.items(): + mqtt_client.publish('{}/{}/{}/{}'.format(base_topic, device_id, sensor_name, param), value, 1, False) + sleep(0.5) # some slack for the publish roundtrip and callback function + elif reporting_mode == 'mqtt-smarthome': + for [param, value] in data.items(): + print_line('Publishing data to MQTT topic "{}/status/{}/{}"'.format(base_topic, sensor_name, param)) + payload = dict() + payload['val'] = value + payload['ts'] = int(round(time() * 1000)) + mqtt_client.publish('{}/status/{}/{}'.format(base_topic, sensor_name, param), json.dumps(payload), retain=True) + sleep(0.5) # some slack for the publish roundtrip and callback function + elif reporting_mode == 'wirenboard-mqtt': + for [param, value] in data.items(): + print_line('Publishing data to MQTT topic "/devices/{}/controls/{}"'.format(sensor_name, param)) + mqtt_client.publish('/devices/{}/controls/{}'.format(sensor_name, param), value, retain=True) + mqtt_client.publish('/devices/{}/controls/{}'.format(sensor_name, 'timestamp'), strftime('%Y-%m-%d %H:%M:%S', localtime()), retain=True) + sleep(0.5) # some slack for the publish roundtrip and callback function + elif reporting_mode == 'json': + data['timestamp'] = strftime('%Y-%m-%d %H:%M:%S', localtime()) + data['name'] = sensor_name + data['name_pretty'] = sensor['name_pretty'] + data['mac'] = sensor['mac'] + data['firmware'] = sensor['firmware'] + print('Data for "{}": {}'.format(sensor_name, json.dumps(data))) + else: + raise NameError('Unexpected reporting_mode.') + print() # Load configuration file config_dir = parse_args.config_dir @@ -136,18 +280,18 @@ def flores_to_openhab_items(flores, reporting_mode): elif reporting_mode == 'wirenboard-mqtt': default_base_topic = '' else: - default_base_topic = 'miflora' + default_base_topic = 'misensor' base_topic = config['MQTT'].get('base_topic', default_base_topic).lower() device_id = config['MQTT'].get('homie_device_id', 'miflora-mqtt-daemon').lower() sleep_period = config['Daemon'].getint('period', 300) -miflora_cache_timeout = sleep_period - 1 +sensor_cache_timeout = sleep_period - 1 # Check configuration if reporting_mode not in ['mqtt-json', 'mqtt-homie', 'json', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json', 'wirenboard-mqtt']: print_line('Configuration parameter reporting_mode set to an invalid value', error=True, sd_notify=True) sys.exit(1) -if not config['Sensors']: +if not config['MiFlora'] or not config['MiTempBt']: print_line('No sensors found in configuration file "config.ini"', error=True, sd_notify=True) sys.exit(1) if reporting_mode == 'wirenboard-mqtt' and base_topic: @@ -198,77 +342,42 @@ def flores_to_openhab_items(flores, reporting_mode): sd_notifier.notify('READY=1') -# Initialize Mi Flora sensors -flores = OrderedDict() -for [name, mac] in config['Sensors'].items(): - if not re.match("C4:7C:8D:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}", mac): - print_line('The MAC address "{}" seems to be in the wrong format. Please check your configuration'.format(mac), error=True, sd_notify=True) - sys.exit(1) - - if '@' in name: - name_pretty, location_pretty = name.split('@') - else: - name_pretty, location_pretty = name, '' - name_clean = clean_identifier(name_pretty) - location_clean = clean_identifier(location_pretty) - - flora = dict() - print('Adding sensor to device list and testing connection ...') - print('Name: "{}"'.format(name_pretty)) - #print_line('Attempting initial connection to Mi Flora sensor "{}" ({})'.format(name_pretty, mac), console=False, sd_notify=True) - - flora_poller = MiFloraPoller(mac=mac, backend=GatttoolBackend, cache_timeout=miflora_cache_timeout, retries=3, adapter=used_adapter) - flora['poller'] = flora_poller - flora['name_pretty'] = name_pretty - flora['mac'] = flora_poller._mac - flora['refresh'] = sleep_period - flora['location_clean'] = location_clean - flora['location_pretty'] = location_pretty - flora['stats'] = {"count": 0, "success": 0, "failure": 0} - try: - flora_poller.fill_cache() - flora_poller.parameter_value(MI_LIGHT) - flora['firmware'] = flora_poller.firmware_version() - except (IOError, BluetoothBackendException): - print_line('Initial connection to Mi Flora sensor "{}" ({}) failed.'.format(name_pretty, mac), error=True, sd_notify=True) - else: - print('Internal name: "{}"'.format(name_clean)) - print('Device name: "{}"'.format(flora_poller.name())) - print('MAC address: {}'.format(flora_poller._mac)) - print('Firmware: {}'.format(flora_poller.firmware_version())) - print_line('Initial connection to Mi Flora sensor "{}" ({}) successful'.format(name_pretty, mac), sd_notify=True) - print() - flores[name_clean] = flora +# Initialize Mi sensors +mifloras = OrderedDict() +init_sensors(sensor_type_miflora, mifloras) +mitempbts = OrderedDict() +init_sensors(sensor_type_mitempbt, mitempbts) # openHAB items generation if parse_args.gen_openhab: - flores_to_openhab_items(flores, reporting_mode) + sensors_to_openhab_items(sensor_type_miflora, mifloras, miflora_parameters, reporting_mode) + sensors_to_openhab_items(sensor_type_mitempbt, mitempbts, mitempbt_parameters, reporting_mode) sys.exit(0) # Discovery Announcement if reporting_mode == 'mqtt-json': - print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...') - flores_info = dict() - for [flora_name, flora] in flores.items(): - flora_info = {key: value for key, value in flora.items() if key not in ['poller', 'stats']} - flora_info['topic'] = '{}/{}'.format(base_topic, flora_name) - flores_info[flora_name] = flora_info - mqtt_client.publish('{}/$announce'.format(base_topic), json.dumps(flores_info), retain=True) + print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_type_miflora,sensor_type_mitempbt)) + sensors_info = dict() + for [sensor_name, sensor] in chain(mifloras.items(),mitempbts.items()): + sensor_info = {key: value for key, value in sensor.items() if key not in ['poller', 'stats']} + sensor_info['topic'] = '{}/{}'.format(base_topic, sensor_name) + sensors_info[sensor_name] = sensor_info + mqtt_client.publish('{}/$announce'.format(base_topic), json.dumps(sensors_info), retain=True) sleep(0.5) # some slack for the publish roundtrip and callback function print() elif reporting_mode == 'mqtt-homie': - print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...') + print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_type_miflora,sensor_type_mitempbt)) mqtt_client.publish('{}/{}/$homie'.format(base_topic, device_id), '2.1.0-alpha', 1, True) mqtt_client.publish('{}/{}/$online'.format(base_topic, device_id), 'true', 1, True) mqtt_client.publish('{}/{}/$name'.format(base_topic, device_id), device_id, 1, True) - mqtt_client.publish('{}/{}/$fw/version'.format(base_topic, device_id), flora['firmware'], 1, True) + #mqtt_client.publish('{}/{}/$fw/version'.format(base_topic, device_id), flora['firmware'], 1, True) - nodes_list = ','.join([flora_name for [flora_name, flora] in flores.items()]) + nodes_list = ','.join([sensor_name for [sensor_name, sensor] in chain(mifloras.items(), mitempbts.items())]) mqtt_client.publish('{}/{}/$nodes'.format(base_topic, device_id), nodes_list, 1, True) - for [flora_name, flora] in flores.items(): - topic_path = '{}/{}/{}'.format(base_topic, device_id, flora_name) - mqtt_client.publish('{}/$name'.format(topic_path), flora['name_pretty'], 1, True) + for [sensor_name, sensor] in mifloras.items(): + topic_path = '{}/{}/{}'.format(base_topic, device_id, sensor_name) + mqtt_client.publish('{}/$name'.format(topic_path), sensor['name_pretty'], 1, True) mqtt_client.publish('{}/$type'.format(topic_path), 'miflora', 1, True) mqtt_client.publish('{}/$properties'.format(topic_path), 'battery,conductivity,light,moisture,temperature', 1, True) mqtt_client.publish('{}/battery/$settable'.format(topic_path), 'false', 1, True) @@ -291,16 +400,34 @@ def flores_to_openhab_items(flores, reporting_mode): mqtt_client.publish('{}/temperature/$unit'.format(topic_path), '°C', 1, True) mqtt_client.publish('{}/temperature/$datatype'.format(topic_path), 'float', 1, True) mqtt_client.publish('{}/temperature/$range'.format(topic_path), '*', 1, True) + + for [sensor_name, sensor] in mitempbts.items(): + topic_path = '{}/{}/{}'.format(base_topic, device_id, sensor_name) + mqtt_client.publish('{}/$name'.format(topic_path), sensor['name_pretty'], 1, True) + mqtt_client.publish('{}/$type'.format(topic_path), 'mitempbt', 1, True) + mqtt_client.publish('{}/$properties'.format(topic_path), 'battery,humidity,temperature', 1, True) + mqtt_client.publish('{}/battery/$settable'.format(topic_path), 'false', 1, True) + mqtt_client.publish('{}/battery/$unit'.format(topic_path), 'percent', 1, True) + mqtt_client.publish('{}/battery/$datatype'.format(topic_path), 'int', 1, True) + mqtt_client.publish('{}/battery/$range'.format(topic_path), '0:100', 1, True) + mqtt_client.publish('{}/humidity/$settable'.format(topic_path), 'false', 1, True) + mqtt_client.publish('{}/humidity/$unit'.format(topic_path), 'percent', 1, True) + mqtt_client.publish('{}/humidity/$datatype'.format(topic_path), 'int', 1, True) + mqtt_client.publish('{}/humidity/$range'.format(topic_path), '0:100', 1, True) + mqtt_client.publish('{}/temperature/$settable'.format(topic_path), 'false', 1, True) + mqtt_client.publish('{}/temperature/$unit'.format(topic_path), '°C', 1, True) + mqtt_client.publish('{}/temperature/$datatype'.format(topic_path), 'float', 1, True) + mqtt_client.publish('{}/temperature/$range'.format(topic_path), '*', 1, True) sleep(0.5) # some slack for the publish roundtrip and callback function print() elif reporting_mode == 'homeassistant-mqtt': - print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...') - for [flora_name, flora] in flores.items(): + print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_type_miflora,sensor_type_mitempbt)) + for [flora_name, flora] in mifloras.items(): topic_path = '{}/sensor/{}'.format(base_topic, flora_name) base_payload = { "state_topic": "{}/state".format(topic_path).lower() } - for sensor, params in parameters.items(): + for sensor, params in miflora_parameters.items(): payload = dict(base_payload.items()) payload['unit_of_measurement'] = params['unit'] payload['value_template'] = "{{ value_json.%s }}" % (sensor, ) @@ -308,9 +435,22 @@ def flores_to_openhab_items(flores, reporting_mode): if 'device_class' in params: payload['device_class'] = params['device_class'] mqtt_client.publish('{}/{}_{}/config'.format(topic_path, flora_name, sensor).lower(), json.dumps(payload), 1, True) + for [mitempbt_name, mitempbt] in mitempbts.items(): + topic_path = '{}/sensor/{}'.format(base_topic, mitempbt_name) + base_payload = { + "state_topic": "{}/state".format(topic_path).lower() + } + for sensor, params in mitempbt_parameters.items(): + payload = dict(base_payload.items()) + payload['unit_of_measurement'] = params['unit'] + payload['value_template'] = "{{ value_json.%s }}" % (sensor, ) + payload['name'] = "{} {}".format(mitempbt_name, sensor.title()) + if 'device_class' in params: + payload['device_class'] = params['device_class'] + mqtt_client.publish('{}/{}_{}/config'.format(topic_path, mitempbt_name, sensor).lower(), json.dumps(payload), 1, True) elif reporting_mode == 'wirenboard-mqtt': - print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...') - for [flora_name, flora] in flores.items(): + print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_type_miflora, sensor_type_mitempbt)) + for [flora_name, flora] in mifloras.items(): mqtt_client.publish('/devices/{}/meta/name'.format(flora_name), flora_name, 1, True) topic_path = '/devices/{}/controls'.format(flora_name) mqtt_client.publish('{}/battery/meta/type'.format(topic_path), 'value', 1, True) @@ -322,90 +462,24 @@ def flores_to_openhab_items(flores, reporting_mode): mqtt_client.publish('{}/moisture/meta/type'.format(topic_path), 'rel_humidity', 1, True) mqtt_client.publish('{}/temperature/meta/type'.format(topic_path), 'temperature', 1, True) mqtt_client.publish('{}/timestamp/meta/type'.format(topic_path), 'text', 1, True) + + for [mitempbt_name, mitempbt] in mitempbts.items(): + mqtt_client.publish('/devices/{}/meta/name'.format(mitempbt_name), mitempbt_name, 1, True) + topic_path = '/device s/{}/controls'.format(mitempbt_name) + mqtt_client.publish('{}/battery/meta/type'.format(topic_path), 'value', 1, True) + mqtt_client.publish('{}/battery/meta/units'.format(topic_path), '%', 1, True) + mqtt_client.publish('{}/humidity/meta/type'.format(topic_path), 'rel_humidity', 1, True) + mqtt_client.publish('{}/temperature/meta/type'.format(topic_path), 'temperature', 1, True) + mqtt_client.publish('{}/timestamp/meta/type'.format(topic_path), 'text', 1, True) sleep(0.5) # some slack for the publish roundtrip and callback function print() print_line('Initialization complete, starting MQTT publish loop', console=False, sd_notify=True) - # Sensor data retrieval and publication while True: - for [flora_name, flora] in flores.items(): - data = dict() - attempts = 2 - flora['poller']._cache = None - flora['poller']._last_read = None - flora['stats']['count'] = flora['stats']['count'] + 1 - print_line('Retrieving data from sensor "{}" ...'.format(flora['name_pretty'])) - while attempts != 0 and not flora['poller']._cache: - try: - flora['poller'].fill_cache() - flora['poller'].parameter_value(MI_LIGHT) - except (IOError, BluetoothBackendException): - attempts = attempts - 1 - if attempts > 0: - print_line('Retrying ...', warning = True) - flora['poller']._cache = None - flora['poller']._last_read = None - - if not flora['poller']._cache: - flora['stats']['failure'] = flora['stats']['failure'] + 1 - print_line('Failed to retrieve data from Mi Flora sensor "{}" ({}), success rate: {:.0%}'.format( - flora['name_pretty'], flora['mac'], flora['stats']['success']/flora['stats']['count'] - ), error = True, sd_notify = True) - print() - continue - else: - flora['stats']['success'] = flora['stats']['success'] + 1 - - for param,_ in parameters.items(): - data[param] = flora['poller'].parameter_value(param) - print_line('Result: {}'.format(json.dumps(data))) - - if reporting_mode == 'mqtt-json': - print_line('Publishing to MQTT topic "{}/{}"'.format(base_topic, flora_name)) - mqtt_client.publish('{}/{}'.format(base_topic, flora_name), json.dumps(data)) - sleep(0.5) # some slack for the publish roundtrip and callback function - elif reporting_mode == 'thingsboard-json': - print_line('Publishing to MQTT topic "{}" username "{}"'.format(base_topic, flora_name)) - mqtt_client.username_pw_set(flora_name) - mqtt_client.reconnect() - sleep(1.0) - mqtt_client.publish('{}'.format(base_topic), json.dumps(data)) - sleep(0.5) # some slack for the publish roundtrip and callback function - elif reporting_mode == 'homeassistant-mqtt': - print_line('Publishing to MQTT topic "{}/sensor/{}/state"'.format(base_topic, flora_name).lower()) - mqtt_client.publish('{}/sensor/{}/state'.format(base_topic, flora_name).lower(), json.dumps(data)) - sleep(0.5) # some slack for the publish roundtrip and callback function - elif reporting_mode == 'mqtt-homie': - print_line('Publishing data to MQTT base topic "{}/{}/{}"'.format(base_topic, device_id, flora_name)) - for [param, value] in data.items(): - mqtt_client.publish('{}/{}/{}/{}'.format(base_topic, device_id, flora_name, param), value, 1, False) - sleep(0.5) # some slack for the publish roundtrip and callback function - elif reporting_mode == 'mqtt-smarthome': - for [param, value] in data.items(): - print_line('Publishing data to MQTT topic "{}/status/{}/{}"'.format(base_topic, flora_name, param)) - payload = dict() - payload['val'] = value - payload['ts'] = int(round(time() * 1000)) - mqtt_client.publish('{}/status/{}/{}'.format(base_topic, flora_name, param), json.dumps(payload), retain=True) - sleep(0.5) # some slack for the publish roundtrip and callback function - elif reporting_mode == 'wirenboard-mqtt': - for [param, value] in data.items(): - print_line('Publishing data to MQTT topic "/devices/{}/controls/{}"'.format(flora_name, param)) - mqtt_client.publish('/devices/{}/controls/{}'.format(flora_name, param), value, retain=True) - mqtt_client.publish('/devices/{}/controls/{}'.format(flora_name, 'timestamp'), strftime('%Y-%m-%d %H:%M:%S', localtime()), retain=True) - sleep(0.5) # some slack for the publish roundtrip and callback function - elif reporting_mode == 'json': - data['timestamp'] = strftime('%Y-%m-%d %H:%M:%S', localtime()) - data['name'] = flora_name - data['name_pretty'] = flora['name_pretty'] - data['mac'] = flora['mac'] - data['firmware'] = flora['firmware'] - print('Data for "{}": {}'.format(flora_name, json.dumps(data))) - else: - raise NameError('Unexpected reporting_mode.') - print() + pool_sensors(sensor_type_miflora, mifloras, miflora_parameters) + pool_sensors(sensor_type_mitempbt, mitempbts, mitempbt_parameters) print_line('Status messages published', console=False, sd_notify=True) @@ -417,4 +491,4 @@ def flores_to_openhab_items(flores, reporting_mode): print_line('Execution finished in non-daemon-mode', sd_notify=True) if reporting_mode == 'mqtt-json': mqtt_client.disconnect() - break + break \ No newline at end of file From 4c41adf740eb9e08bc4a1101fab462ef5a7ac609 Mon Sep 17 00:00:00 2001 From: aqualx Date: Tue, 29 Jan 2019 00:22:01 +0200 Subject: [PATCH 05/15] minor changes --- miflora-mqtt-daemon.py | 3 ++- requirements.txt | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/miflora-mqtt-daemon.py b/miflora-mqtt-daemon.py index dfc3f8b..41e0604 100755 --- a/miflora-mqtt-daemon.py +++ b/miflora-mqtt-daemon.py @@ -23,7 +23,8 @@ project_url = 'https://github.com/ThomDietrich/miflora-mqtt-daemon' sensor_type_miflora = "Mi Flora" -sensor_type_mitempbt = "Mi Temp & Humidity" +sensor_type_mitempbt = "Mi Smart Temperature & Humidity" + miflora_parameters = OrderedDict([ (MI_LIGHT, dict(name="LightIntensity", name_pretty='Sunlight Intensity', typeformat='%d', unit='lux', device_class="illuminance")), (MI_TEMPERATURE, dict(name="AirTemperature", name_pretty='Air Temperature', typeformat='%.1f', unit='°C', device_class="temperature")), diff --git a/requirements.txt b/requirements.txt index 8717c2f..39ee074 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,5 @@ wheel==0.29.0 sdnotify==0.3.1 colorama==0.3.9 Unidecode==0.4.21 +bluepy==1.3.0 +mitemp-bt==0.0.1 \ No newline at end of file From abc43a3c13970607e2796f215d7247782ef027f8 Mon Sep 17 00:00:00 2001 From: Oleksandr Kurbatov <45320692+aqualx@users.noreply.github.com> Date: Tue, 29 Jan 2019 14:03:03 +0200 Subject: [PATCH 06/15] New device support Added information about Xiaomi Mijia Temperature and Humidity Sensor --- README.md | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3030632..d01f1fc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon -A simple Linux python script to query arbitrary Mi Flora plant sensor devices and send the data to an **MQTT** broker, +A simple Linux python script to query arbitrary Mi Bluetooth sensor devices and send the data to an **MQTT** broker, e.g., the famous [Eclipse Mosquitto](https://projects.eclipse.org/projects/technology.mosquitto). 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. @@ -9,15 +9,26 @@ After data made the hop to the MQTT broker it can be used by home automation sof The program can be executed in **daemon mode** to run continuously in the background, e.g., as a systemd service. ## About Mi Flora -* [Xiaomi Mi Flora sensors](https://xiaomi-mi.com/sockets-and-sensors/xiaomi-huahuacaocao-flower-care-smart-monitor) ([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 +* [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 * The sensor uses Bluetooth Low Energy (BLE) and has a rather limited range * A coin cell battery is used as power source, which should last between 1.5 to 2 years under normal conditions * 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) +## About Xiaomi Mijia Temperature and Humidity Sensor +* ''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 +* The sensor uses Bluetooth Low Energy (BLE) and has a rather limited range +* Weight: 43 g +* Screen size: 1.78 inch +* Temperature range: -9.9-60 +* Humidity range: 0 ~ 99.9% +* Rated power: 0.18 mW +* Powered By : Batteries (AAA) + ## Features * Tested with Mi Flora firmware v2.6.2, v2.6.4, v2.6.6, v3.1.4, others anticipated -* Build on top of [open-homeautomation/miflora](https://github.com/open-homeautomation/miflora) +* Tested with Xiaomi Mijia Temperature and Humidity Sensor (MJ_HT_V1) firmware v00.00.66 +* Build on top of [open-homeautomation/miflora](https://github.com/open-homeautomation/miflora) and [mitemp_bt](https://github.com/flavio20002/mitemp_bt) * Highly configurable * Data publication via MQTT * Configurable topic and payload: @@ -36,9 +47,9 @@ The program can be executed in **daemon mode** to run continuously in the backgr * Automatic generation of openHAB items and rules * Reliable and intuitive * Tested on Raspberry Pi 3 and Raspberry Pi 0W +* Wiren Board 5 (Debian Stretch) - -![Promotional image](https://xiaomi-mi.com/uploads/ck/xiaomi-flower-monitor-001.jpg) +![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) ### Readings @@ -52,6 +63,14 @@ The Mi Flora sensor offers the following plant and soil readings: | `conductivity` | [Soil fertility](https://www.plantcaretools.com/measure-fertilization-with-ec-meters-for-plants-faq), in [µS/cm] | | `battery` | Sensor battery level, in [%] | +The Xiaomi Mijia Temperature and Humidity Sensor offers the following plant and soil readings: + +| Name | Description | +|-----------------|-------------| +| `temperature` | Air temperature, in [°C] (0.1°C resolution) | +| `humidity` | Air humidity in [%] | +| `battery` | Sensor battery level, in [%] | + ## Prerequisites An MQTT broker is needed as the counterpart for this daemon. @@ -91,13 +110,13 @@ vim /opt/miflora-mqtt-daemon/config.ini **Attention:** You need to add at least one sensor to the configuration. -Scan for available Mi Flora sensors in your proximity with the command: +Scan for available Mi Bluetooth sensors in your proximity with the command: ```shell sudo hcitool lescan ``` -Interfacing your Mi Flora sensor with this program is harmless. +Interfacing your Mi Bluetooth sensor with this program is harmless. The device will not be modified and will still work with the official Xiaomi app. ## Execution From 8ac6342d5bfd01c6d08cd5e5f8a3604773d8cc49 Mon Sep 17 00:00:00 2001 From: Oleksandr Kurbatov <45320692+aqualx@users.noreply.github.com> Date: Tue, 29 Jan 2019 14:11:36 +0200 Subject: [PATCH 07/15] minor changes --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d01f1fc..236d63b 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ The program can be executed in **daemon mode** to run continuously in the backgr * A coin cell battery is used as power source, which should last between 1.5 to 2 years under normal conditions * 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) +![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) + ## About Xiaomi Mijia Temperature and Humidity Sensor * ''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 * The sensor uses Bluetooth Low Energy (BLE) and has a rather limited range @@ -24,6 +26,8 @@ The program can be executed in **daemon mode** to run continuously in the backgr * Rated power: 0.18 mW * Powered By : Batteries (AAA) +![Promotional image](http://ae01.alicdn.com/kf/HTB11qrpeStYBeNjSspkq6zU8VXax.jpg) + ## Features * Tested with Mi Flora firmware v2.6.2, v2.6.4, v2.6.6, v3.1.4, others anticipated @@ -49,8 +53,6 @@ The program can be executed in **daemon mode** to run continuously in the backgr * Tested on Raspberry Pi 3 and Raspberry Pi 0W * Wiren Board 5 (Debian Stretch) -![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) - ### Readings The Mi Flora sensor offers the following plant and soil readings: @@ -93,7 +95,7 @@ sudo pip3 install -r requirements.txt The daemon depends on `gatttool`, an external tool provided by the package `bluez` installed just now. Make sure gatttool is available on your system by executing the command once: - + ```shell gatttool --help ``` From a8c550953a1a512b114b0ede80b192c5bf1147f7 Mon Sep 17 00:00:00 2001 From: Oleksandr Kurbatov <45320692+aqualx@users.noreply.github.com> Date: Tue, 29 Jan 2019 14:12:36 +0200 Subject: [PATCH 08/15] minor changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 236d63b..db569b1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon +# Xiaomi Mi Bluetooth Sensor MQTT Client/Daemon A simple Linux python script to query arbitrary Mi Bluetooth sensor devices and send the data to an **MQTT** broker, e.g., the famous [Eclipse Mosquitto](https://projects.eclipse.org/projects/technology.mosquitto). From 83afc34e574c0e4443c46b7510c08b73ecdcb34d Mon Sep 17 00:00:00 2001 From: Oleksandr Kurbatov <45320692+aqualx@users.noreply.github.com> Date: Tue, 29 Jan 2019 21:56:26 +0200 Subject: [PATCH 09/15] Minor --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index db569b1..7244ed1 100644 --- a/README.md +++ b/README.md @@ -21,10 +21,10 @@ The program can be executed in **daemon mode** to run continuously in the backgr * The sensor uses Bluetooth Low Energy (BLE) and has a rather limited range * Weight: 43 g * Screen size: 1.78 inch -* Temperature range: -9.9-60 -* Humidity range: 0 ~ 99.9% +* Temperature range: -9.9°C-60°C +* Humidity range: 0~99.9% * Rated power: 0.18 mW -* Powered By : Batteries (AAA) +* Powered By: Battery (AAA) ![Promotional image](http://ae01.alicdn.com/kf/HTB11qrpeStYBeNjSspkq6zU8VXax.jpg) @@ -65,7 +65,7 @@ The Mi Flora sensor offers the following plant and soil readings: | `conductivity` | [Soil fertility](https://www.plantcaretools.com/measure-fertilization-with-ec-meters-for-plants-faq), in [µS/cm] | | `battery` | Sensor battery level, in [%] | -The Xiaomi Mijia Temperature and Humidity Sensor offers the following plant and soil readings: +The Xiaomi Mijia Temperature and Humidity Sensor offers the following readings: | Name | Description | |-----------------|-------------| From aed49afe559788630e4836ea3387d1318c962a6c Mon Sep 17 00:00:00 2001 From: Oleksandr Kurbatov <45320692+aqualx@users.noreply.github.com> Date: Tue, 29 Jan 2019 22:01:31 +0200 Subject: [PATCH 10/15] Fixed MAC address check for mitemp_bt sensors --- miflora-mqtt-daemon.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/miflora-mqtt-daemon.py b/miflora-mqtt-daemon.py index 41e0604..fb29b98 100755 --- a/miflora-mqtt-daemon.py +++ b/miflora-mqtt-daemon.py @@ -133,7 +133,7 @@ def init_sensors(type, sensors): mac_regexp = "C4:7C:8D:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}" elif type == sensor_type_mitempbt: config_section = "MiTempBt" - mac_regexp = "4C:65:A8:DB:[0-9A-F]{2}:[0-9A-F]{2}" + mac_regexp = "4C:65:A8:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}" else: print_line('Unknown device type: {}'.format(type), error=True, sd_notify=True) sys.exit(1) @@ -492,4 +492,4 @@ def pool_sensors(type, sensors, parameters): print_line('Execution finished in non-daemon-mode', sd_notify=True) if reporting_mode == 'mqtt-json': mqtt_client.disconnect() - break \ No newline at end of file + break From 51ad7d6320cf14faeec72fdcdf0c081f29acc0f3 Mon Sep 17 00:00:00 2001 From: aqualx Date: Thu, 31 Jan 2019 00:15:19 +0200 Subject: [PATCH 11/15] * added separate setting "period" per device type in config.ini * switched library 'Mijia Bluetooth Temperature Smart Humidity' from mitemp_bt to mithermometer * fixed schema generation for openHAB * set 'retain=True' for mqtt messages for homeassistant-mqtt * other minor fixes --- config.ini.dist | 10 ++- miflora-mqtt-daemon.py | 145 ++++++++++++++++++++++++++--------------- requirements.txt | 3 +- 3 files changed, 101 insertions(+), 57 deletions(-) diff --git a/config.ini.dist b/config.ini.dist index 36ee461..7a5a7fd 100644 --- a/config.ini.dist +++ b/config.ini.dist @@ -23,7 +23,7 @@ # #reporting_method = mqtt-json -# The bluetooth adapter that should be used to connect to Mi Flora devices (Default: hci0) +# The bluetooth adapter that should be used to connect to Mi Bluetooth devices (Default: hci0) #adapter = hci0 [Daemon] @@ -31,8 +31,11 @@ # Enable or Disable an endless execution loop (Default: true) #enabled = true -# The period between two measurements in seconds (Default: 300) -#period = 300 +# The period between two measurements in seconds for MiFlora sensors (Default: 300) +#period_miflora = 300 + +# The period between two measurements in seconds for MiTempBt sensors (Default: 60) +#period_mitempbt = 60 [MQTT] @@ -86,4 +89,5 @@ #Petunia@Balcony = C4:7C:8D:77:88:99 [MiTempBt] + # Add your Mi Temerature & Humidity sensors here. Setup is same as for [MiFlora] section \ No newline at end of file diff --git a/miflora-mqtt-daemon.py b/miflora-mqtt-daemon.py index 41e0604..ddfe98d 100755 --- a/miflora-mqtt-daemon.py +++ b/miflora-mqtt-daemon.py @@ -6,6 +6,7 @@ import json import os.path import argparse +import threading from itertools import chain from time import time, sleep, localtime, strftime from collections import OrderedDict @@ -14,16 +15,18 @@ from configparser import ConfigParser from unidecode import unidecode from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE -from btlewrap import available_backends, BluepyBackend, GatttoolBackend, PygattBackend, BluetoothBackendException -from mitemp_bt.mitemp_bt_poller import MiTempBtPoller, MI_HUMIDITY +from mithermometer.mithermometer_poller import MiThermometerPoller, MI_HUMIDITY +from btlewrap.bluepy import BluepyBackend, BluetoothBackendException import paho.mqtt.client as mqtt import sdnotify project_name = 'Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon' project_url = 'https://github.com/ThomDietrich/miflora-mqtt-daemon' -sensor_type_miflora = "Mi Flora" -sensor_type_mitempbt = "Mi Smart Temperature & Humidity" +sensor_name_miflora = "Mi Flora" +sensor_type_miflora = "MiFlora" +sensor_name_mitempbt = "Mijia Bluetooth Temperature Smart Humidity" +sensor_type_mitempbt = "MiTempBt" miflora_parameters = OrderedDict([ (MI_LIGHT, dict(name="LightIntensity", name_pretty='Sunlight Intensity', typeformat='%d', unit='lux', device_class="illuminance")), @@ -73,6 +76,10 @@ def print_line(text, error = False, warning=False, sd_notify=False, console=True if sd_notify: sd_notifier.notify('STATUS={} - {}.'.format(timestamp_sd, unidecode(text))) +# convert device type to human-readable name +def sensor_type_to_name(sensor_type): + return sensor_name_miflora if (sensor_type == sensor_type_miflora ) else sensor_name_mitempbt + # Identifier cleanup def clean_identifier(name): clean = name.strip() @@ -97,23 +104,24 @@ def on_publish(client, userdata, mid): pass -def sensors_to_openhab_items(type, sensors, sensor_params, reporting_mode): +def sensors_to_openhab_items(sensor_type, sensors, sensor_params, reporting_mode): + sensor_type_name = sensor_type_to_name(sensor_type) print_line('Generating openHAB items. Copy to your configuration and modify as needed...') items = list() - items.append('// miflora.items - Generated by miflora-mqtt-daemon.') + items.append('// {}.items - Generated by miflora-mqtt-daemon.'.format(sensor_type.lower())) items.append('// Adapt to your needs! Things you probably want to modify:') items.append('// Room group names, icons,') items.append('// "gAll", "broker", "UnknownRoom"') items.append('') - items.append('// {} specific groups').format(type) - items.append('Group gMiFlora "All {} sensors and elements" (gAll)'.format(type)) + items.append('// {} specific groups'.format(sensor_type_name)) + items.append('Group g{} "All {} sensors and elements" (gAll)'.format(sensor_type, sensor_type_name)) for param, param_properties in sensor_params.items(): - items.append('Group g{} "{} {} elements" (gAll, gMiFlora)'.format(param_properties['name'], type, param_properties['name_pretty'])) + items.append('Group g{} "{} {} elements" (gAll, g{})'.format(param_properties['name'], sensor_type_name, param_properties['name_pretty'], sensor_type)) if reporting_mode == 'mqtt-json': for [sensor_name, sensor] in sensors.items(): location = sensor['location_clean'] if sensor['location_clean'] else 'UnknownRoom' - items.append('\n// {} "{}" ({})'.format(type, flora['name_pretty'], flora['mac'])) - items.append('Group g{}{} "{} Sensor {}" (gMiFlora, g{})'.format(location, flora_name, type, sensor['name_pretty'], location)) + items.append('\n// {} "{}" ({})'.format(sensor_type_name, sensor['name_pretty'], sensor['mac'])) + items.append('Group g{}{} "{} Sensor {}" (g{}, g{})'.format(location, sensor_name, sensor_type_name, sensor['name_pretty'], sensor_type, location)) for [param, param_properties] in sensor_params.items(): basic = 'Number {}_{}_{}'.format(location, sensor_name, param_properties['name']) label = '"{} {} {} [{} {}]"'.format(location, sensor['name_pretty'], param_properties['name_pretty'], param_properties['typeformat'], param_properties['unit'].replace('%', '%%')) @@ -127,15 +135,16 @@ def sensors_to_openhab_items(type, sensors, sensor_params, reporting_mode): raise IOError('Given reporting_mode not supported for the export to openHAB items') # Init sensors from configuration files -def init_sensors(type, sensors): - if type == sensor_type_miflora: - config_section = "MiFlora" +def init_sensors(sensor_type, sensors): + sensor_type_name = sensor_type_to_name(sensor_type) + if sensor_type == sensor_type_miflora: + config_section = sensor_type_miflora mac_regexp = "C4:7C:8D:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}" - elif type == sensor_type_mitempbt: - config_section = "MiTempBt" - mac_regexp = "4C:65:A8:DB:[0-9A-F]{2}:[0-9A-F]{2}" + elif sensor_type == sensor_type_mitempbt: + config_section = sensor_type_mitempbt + mac_regexp = "4C:65:A8:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}" else: - print_line('Unknown device type: {}'.format(type), error=True, sd_notify=True) + print_line('Unknown device type: {}'.format(sensor_type), error=True, sd_notify=True) sys.exit(1) for [name, mac] in config[config_section].items(): @@ -155,15 +164,15 @@ def init_sensors(type, sensors): print('Name: "{}"'.format(name_pretty)) #print_line('Attempting initial connection to Mi Flora sensor "{}" ({})'.format(name_pretty, mac), console=False, sd_notify=True) - if type == sensor_type_miflora: - sensor_poller = MiFloraPoller(mac=mac, backend=GatttoolBackend, cache_timeout=sensor_cache_timeout, retries=3, adapter=used_adapter) - elif type == sensor_type_mitempbt: - sensor_poller = MiTempBtPoller(mac=mac, backend=BluepyBackend, cache_timeout=sensor_cache_timeout, retries=3, adapter=used_adapter) + if sensor_type == sensor_type_miflora: + sensor_poller = MiFloraPoller(mac=mac, backend=BluepyBackend, cache_timeout=miflora_cache_timeout, retries=3, adapter=used_adapter) + elif sensor_type == sensor_type_mitempbt: + sensor_poller = MiThermometerPoller(mac=mac, backend=BluepyBackend, cache_timeout=mitempbt_cache_timeout, retries=3, adapter=used_adapter) sensor['poller'] = sensor_poller sensor['name_pretty'] = name_pretty sensor['mac'] = sensor_poller._mac - sensor['refresh'] = sleep_period + sensor['refresh'] = miflora_sleep_period if (sensor_type == sensor_type_miflora) else mitempbt_sleep_period sensor['location_clean'] = location_clean sensor['location_pretty'] = location_pretty sensor['stats'] = {"count": 0, "success": 0, "failure": 0} @@ -172,25 +181,26 @@ def init_sensors(type, sensors): sensor_poller.parameter_value(MI_BATTERY) sensor['firmware'] = sensor_poller.firmware_version() except (IOError, BluetoothBackendException): - print_line('Initial connection to {} sensor "{}" ({}) failed.'.format(type, name_pretty, mac), error=True, sd_notify=True) + print_line('Initial connection to {} sensor "{}" ({}) failed.'.format(sensor_type_name, name_pretty, mac), error=True, sd_notify=True) else: print('Internal name: "{}"'.format(name_clean)) print('Device name: "{}"'.format(sensor_poller.name())) print('MAC address: {}'.format(sensor_poller._mac)) print('Firmware: {}'.format(sensor_poller.firmware_version())) - print_line('Initial connection to {} sensor "{}" ({}) successful'.format(type, name_pretty, mac), sd_notify=True) + print_line('Initial connection to {} sensor "{}" ({}) successful'.format(sensor_type_name, name_pretty, mac), sd_notify=True) print() sensors[name_clean] = sensor # Pool & publish information from sensors -def pool_sensors(type, sensors, parameters): +def pool_sensors(sensor_type, sensors, parameters): + sensor_type_name = sensor_type_to_name(sensor_type) for [sensor_name, sensor] in sensors.items(): data = dict() attempts = 2 sensor['poller']._cache = None sensor['poller']._last_read = None sensor['stats']['count'] = sensor['stats']['count'] + 1 - print_line('Retrieving data from {} sensor "{}" ...'.format(type, sensor['name_pretty'])) + print_line('Retrieving data from {} sensor "{}" ...'.format(sensor_type_name, sensor['name_pretty'])) while attempts != 0 and not sensor['poller']._cache: try: sensor['poller'].fill_cache() @@ -205,7 +215,7 @@ def pool_sensors(type, sensors, parameters): if not sensor['poller']._cache: sensor['stats']['failure'] = sensor['stats']['failure'] + 1 print_line('Failed to retrieve data from {} sensor "{}" ({}), success rate: {:.0%}'.format( - type, sensor['name_pretty'], sensor['mac'], sensor['stats']['success']/sensor['stats']['count'] + sensor_type_name, sensor['name_pretty'], sensor['mac'], sensor['stats']['success']/sensor['stats']['count'] ), error = True, sd_notify = True) print() continue @@ -229,12 +239,12 @@ def pool_sensors(type, sensors, parameters): sleep(0.5) # some slack for the publish roundtrip and callback function elif reporting_mode == 'homeassistant-mqtt': print_line('Publishing to MQTT topic "{}/sensor/{}/state"'.format(base_topic, sensor_name).lower()) - mqtt_client.publish('{}/sensor/{}/state'.format(base_topic, sensor_name).lower(), json.dumps(data)) + mqtt_client.publish('{}/sensor/{}/state'.format(base_topic, sensor_name).lower(), json.dumps(data), retain=True) sleep(0.5) # some slack for the publish roundtrip and callback function elif reporting_mode == 'mqtt-homie': print_line('Publishing data to MQTT base topic "{}/{}/{}"'.format(base_topic, device_id, sensor_name)) for [param, value] in data.items(): - mqtt_client.publish('{}/{}/{}/{}'.format(base_topic, device_id, sensor_name, param), value, 1, False) + mqtt_client.publish('{}/{}/{}/{}'.format(base_topic, device_id, sensor_name, param), value, 1, retain=False) sleep(0.5) # some slack for the publish roundtrip and callback function elif reporting_mode == 'mqtt-smarthome': for [param, value] in data.items(): @@ -285,14 +295,16 @@ def pool_sensors(type, sensors, parameters): base_topic = config['MQTT'].get('base_topic', default_base_topic).lower() device_id = config['MQTT'].get('homie_device_id', 'miflora-mqtt-daemon').lower() -sleep_period = config['Daemon'].getint('period', 300) -sensor_cache_timeout = sleep_period - 1 +miflora_sleep_period = config['Daemon'].getint('period_miflora', 300) +miflora_cache_timeout = miflora_sleep_period - 1 +mitempbt_sleep_period = config['Daemon'].getint('period_mitempbt', 60) +mitempbt_cache_timeout = mitempbt_sleep_period - 1 # Check configuration if reporting_mode not in ['mqtt-json', 'mqtt-homie', 'json', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json', 'wirenboard-mqtt']: print_line('Configuration parameter reporting_mode set to an invalid value', error=True, sd_notify=True) sys.exit(1) -if not config['MiFlora'] or not config['MiTempBt']: +if not config[sensor_type_miflora] or not config[sensor_type_mitempbt]: print_line('No sensors found in configuration file "config.ini"', error=True, sd_notify=True) sys.exit(1) if reporting_mode == 'wirenboard-mqtt' and base_topic: @@ -357,7 +369,7 @@ def pool_sensors(type, sensors, parameters): # Discovery Announcement if reporting_mode == 'mqtt-json': - print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_type_miflora,sensor_type_mitempbt)) + print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_name_miflora,sensor_name_mitempbt)) sensors_info = dict() for [sensor_name, sensor] in chain(mifloras.items(),mitempbts.items()): sensor_info = {key: value for key, value in sensor.items() if key not in ['poller', 'stats']} @@ -367,7 +379,7 @@ def pool_sensors(type, sensors, parameters): sleep(0.5) # some slack for the publish roundtrip and callback function print() elif reporting_mode == 'mqtt-homie': - print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_type_miflora,sensor_type_mitempbt)) + print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_name_miflora,sensor_name_mitempbt)) mqtt_client.publish('{}/{}/$homie'.format(base_topic, device_id), '2.1.0-alpha', 1, True) mqtt_client.publish('{}/{}/$online'.format(base_topic, device_id), 'true', 1, True) mqtt_client.publish('{}/{}/$name'.format(base_topic, device_id), device_id, 1, True) @@ -422,7 +434,7 @@ def pool_sensors(type, sensors, parameters): sleep(0.5) # some slack for the publish roundtrip and callback function print() elif reporting_mode == 'homeassistant-mqtt': - print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_type_miflora,sensor_type_mitempbt)) + print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_name_miflora,sensor_name_mitempbt)) for [flora_name, flora] in mifloras.items(): topic_path = '{}/sensor/{}'.format(base_topic, flora_name) base_payload = { @@ -450,7 +462,7 @@ def pool_sensors(type, sensors, parameters): payload['device_class'] = params['device_class'] mqtt_client.publish('{}/{}_{}/config'.format(topic_path, mitempbt_name, sensor).lower(), json.dumps(payload), 1, True) elif reporting_mode == 'wirenboard-mqtt': - print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_type_miflora, sensor_type_mitempbt)) + print_line('Announcing {}/{} devices to MQTT broker for auto-discovery ...'.format(sensor_name_miflora, sensor_name_mitempbt)) for [flora_name, flora] in mifloras.items(): mqtt_client.publish('/devices/{}/meta/name'.format(flora_name), flora_name, 1, True) topic_path = '/devices/{}/controls'.format(flora_name) @@ -477,19 +489,46 @@ def pool_sensors(type, sensors, parameters): print_line('Initialization complete, starting MQTT publish loop', console=False, sd_notify=True) -# Sensor data retrieval and publication -while True: - pool_sensors(sensor_type_miflora, mifloras, miflora_parameters) - pool_sensors(sensor_type_mitempbt, mitempbts, mitempbt_parameters) - - print_line('Status messages published', console=False, sd_notify=True) - - if daemon_enabled: - print_line('Sleeping ({} seconds) ...'.format(sleep_period)) - sleep(sleep_period) - print() - else: - print_line('Execution finished in non-daemon-mode', sd_notify=True) - if reporting_mode == 'mqtt-json': - mqtt_client.disconnect() - break \ No newline at end of file +class sensorPooler(threading.Thread): + def __init__(self, sensor_type, sensors, sensor_parameters, sleep_period): + threading.Thread.__init__(self) + self.sensor_type = sensor_type + self.sensors = sensors + self.sensor_parameters = sensor_parameters + self.sleep_period = sleep_period + self.daemon = True + def run(self): + sensor_type_name = sensor_type_to_name(self.sensor_type) + print_line('Worker for {} sensors started'.format(sensor_type_name), sd_notify=True) + # Sensor data retrieving and publishing + while True: + hciLock.acquire() + pool_sensors(self.sensor_type, self.sensors, self.sensor_parameters) + hciLock.release() + if daemon_enabled: + print_line('Sleeping for {} ({} seconds) ...'.format(sensor_type_name, self.sleep_period)) + print() + sleep(self.sleep_period) + else: + print_line('Execution finished for {} in non-daemon-mode'.format(sensor_type_name), sd_notify=True) + print() + break + +hciLock = threading.Lock() +threads = [] + +mifloraThread = sensorPooler(sensor_type_miflora, mifloras, miflora_parameters, miflora_sleep_period) +mitempbtThread = sensorPooler(sensor_type_mitempbt, mitempbts, mitempbt_parameters, mitempbt_sleep_period) + +mifloraThread.start() +mitempbtThread.start() + +threads.append(mifloraThread) +threads.append(mitempbtThread) + +for thread in threads: + thread.join() + +print ("Exiting Main Thread") +if reporting_mode == 'mqtt-json': + mqtt_client.disconnect() \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 39ee074..8fec31f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ sdnotify==0.3.1 colorama==0.3.9 Unidecode==0.4.21 bluepy==1.3.0 -mitemp-bt==0.0.1 \ No newline at end of file +btlewrap==0.0.3 +mithermometer==0.1.2 \ No newline at end of file From c96b1bc5b1615ddbfd107d349453bcb451c0f527 Mon Sep 17 00:00:00 2001 From: aqualx Date: Thu, 31 Jan 2019 12:14:18 +0200 Subject: [PATCH 12/15] Fixed crash due to unhandled bluepy.btle.BTLEDisconnectError exception --- miflora-mqtt-daemon.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/miflora-mqtt-daemon.py b/miflora-mqtt-daemon.py index b6422ca..a4fa7f0 100755 --- a/miflora-mqtt-daemon.py +++ b/miflora-mqtt-daemon.py @@ -17,6 +17,7 @@ from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE from mithermometer.mithermometer_poller import MiThermometerPoller, MI_HUMIDITY from btlewrap.bluepy import BluepyBackend, BluetoothBackendException +from bluepy.btle import BTLEDisconnectError import paho.mqtt.client as mqtt import sdnotify @@ -205,7 +206,7 @@ def pool_sensors(sensor_type, sensors, parameters): try: sensor['poller'].fill_cache() sensor['poller'].parameter_value(MI_BATTERY) - except (IOError, BluetoothBackendException): + except (IOError, BluetoothBackendException,BTLEDisconnectError): attempts = attempts - 1 if attempts > 0: print_line('Retrying ...', warning = True) From f30d397cd872ddefdf8f3d5edca71b15561f1e92 Mon Sep 17 00:00:00 2001 From: aqualx Date: Fri, 1 Feb 2019 11:45:49 +0200 Subject: [PATCH 13/15] * Fixes for some crashes * Minor fixes --- miflora-mqtt-daemon.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/miflora-mqtt-daemon.py b/miflora-mqtt-daemon.py index a4fa7f0..60e8926 100755 --- a/miflora-mqtt-daemon.py +++ b/miflora-mqtt-daemon.py @@ -16,7 +16,7 @@ from unidecode import unidecode from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE from mithermometer.mithermometer_poller import MiThermometerPoller, MI_HUMIDITY -from btlewrap.bluepy import BluepyBackend, BluetoothBackendException +from btlewrap import BluepyBackend, BluetoothBackendException from bluepy.btle import BTLEDisconnectError import paho.mqtt.client as mqtt import sdnotify @@ -206,7 +206,7 @@ def pool_sensors(sensor_type, sensors, parameters): try: sensor['poller'].fill_cache() sensor['poller'].parameter_value(MI_BATTERY) - except (IOError, BluetoothBackendException,BTLEDisconnectError): + except (IOError, BluetoothBackendException, BTLEDisconnectError): attempts = attempts - 1 if attempts > 0: print_line('Retrying ...', warning = True) @@ -260,7 +260,7 @@ def pool_sensors(sensor_type, sensors, parameters): print_line('Publishing data to MQTT topic "/devices/{}/controls/{}"'.format(sensor_name, param)) mqtt_client.publish('/devices/{}/controls/{}'.format(sensor_name, param), value, retain=True) mqtt_client.publish('/devices/{}/controls/{}'.format(sensor_name, 'timestamp'), strftime('%Y-%m-%d %H:%M:%S', localtime()), retain=True) - sleep(0.5) # some slack for the publish roundtrip and callback function + sleep(1) # some slack for the publish roundtrip and callback function elif reporting_mode == 'json': data['timestamp'] = strftime('%Y-%m-%d %H:%M:%S', localtime()) data['name'] = sensor_name @@ -305,7 +305,7 @@ def pool_sensors(sensor_type, sensors, parameters): if reporting_mode not in ['mqtt-json', 'mqtt-homie', 'json', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json', 'wirenboard-mqtt']: print_line('Configuration parameter reporting_mode set to an invalid value', error=True, sd_notify=True) sys.exit(1) -if not config[sensor_type_miflora] or not config[sensor_type_mitempbt]: +if not config[sensor_type_miflora] and not config[sensor_type_mitempbt]: print_line('No sensors found in configuration file "config.ini"', error=True, sd_notify=True) sys.exit(1) if reporting_mode == 'wirenboard-mqtt' and base_topic: @@ -519,18 +519,19 @@ def run(self): hciLock = threading.Lock() threads = [] -mifloraThread = sensorPooler(sensor_type_miflora, mifloras, miflora_parameters, miflora_sleep_period) -mitempbtThread = sensorPooler(sensor_type_mitempbt, mitempbts, mitempbt_parameters, mitempbt_sleep_period) - -mifloraThread.start() -mitempbtThread.start() +if len(mifloras) != 0: + mifloraThread = sensorPooler(sensor_type_miflora, mifloras, miflora_parameters, miflora_sleep_period) + mifloraThread.start() + threads.append(mifloraThread) -threads.append(mifloraThread) -threads.append(mitempbtThread) +if len(mitempbts) != 0: + mitempbtThread = sensorPooler(sensor_type_mitempbt, mitempbts, mitempbt_parameters, mitempbt_sleep_period) + mitempbtThread.start() + threads.append(mitempbtThread) for thread in threads: thread.join() print ("Exiting Main Thread") if reporting_mode == 'mqtt-json': - mqtt_client.disconnect() \ No newline at end of file + mqtt_client.disconnect() From ea3065342e71cf90a73fd8d8b44b982297bbcdc9 Mon Sep 17 00:00:00 2001 From: aqualx Date: Thu, 7 Feb 2019 14:57:35 +0200 Subject: [PATCH 14/15] * catch of unhandled crashes from underlying BLE wrappers --- miflora-mqtt-daemon.py | 79 ++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 37 deletions(-) diff --git a/miflora-mqtt-daemon.py b/miflora-mqtt-daemon.py index 60e8926..3f78c67 100755 --- a/miflora-mqtt-daemon.py +++ b/miflora-mqtt-daemon.py @@ -17,9 +17,12 @@ from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE from mithermometer.mithermometer_poller import MiThermometerPoller, MI_HUMIDITY from btlewrap import BluepyBackend, BluetoothBackendException -from bluepy.btle import BTLEDisconnectError +from bluepy.btle import BTLEException import paho.mqtt.client as mqtt import sdnotify +from signal import signal, SIGPIPE, SIG_DFL + +signal(SIGPIPE,SIG_DFL) project_name = 'Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon' project_url = 'https://github.com/ThomDietrich/miflora-mqtt-daemon' @@ -181,7 +184,7 @@ def init_sensors(sensor_type, sensors): sensor_poller.fill_cache() sensor_poller.parameter_value(MI_BATTERY) sensor['firmware'] = sensor_poller.firmware_version() - except (IOError, BluetoothBackendException): + except (IOError, BluetoothBackendException, BTLEException, RuntimeError, BrokenPipeError): print_line('Initial connection to {} sensor "{}" ({}) failed.'.format(sensor_type_name, name_pretty, mac), error=True, sd_notify=True) else: print('Internal name: "{}"'.format(name_clean)) @@ -206,10 +209,12 @@ def pool_sensors(sensor_type, sensors, parameters): try: sensor['poller'].fill_cache() sensor['poller'].parameter_value(MI_BATTERY) - except (IOError, BluetoothBackendException, BTLEDisconnectError): + except (IOError, BluetoothBackendException, BTLEException, RuntimeError, BrokenPipeError) as e: attempts = attempts - 1 if attempts > 0: print_line('Retrying ...', warning = True) + if len(str(e))>0: + print_line('\tDue to: {}'.format(e), error=True) sensor['poller']._cache = None sensor['poller']._last_read = None @@ -260,7 +265,7 @@ def pool_sensors(sensor_type, sensors, parameters): print_line('Publishing data to MQTT topic "/devices/{}/controls/{}"'.format(sensor_name, param)) mqtt_client.publish('/devices/{}/controls/{}'.format(sensor_name, param), value, retain=True) mqtt_client.publish('/devices/{}/controls/{}'.format(sensor_name, 'timestamp'), strftime('%Y-%m-%d %H:%M:%S', localtime()), retain=True) - sleep(1) # some slack for the publish roundtrip and callback function + sleep(0.5) # some slack for the publish roundtrip and callback function elif reporting_mode == 'json': data['timestamp'] = strftime('%Y-%m-%d %H:%M:%S', localtime()) data['name'] = sensor_name @@ -272,6 +277,35 @@ def pool_sensors(sensor_type, sensors, parameters): raise NameError('Unexpected reporting_mode.') print() +class sensorPooler(threading.Thread): + def __init__(self, sensor_type, sensors, sensor_parameters, sleep_period, hciLock): + threading.Thread.__init__(self) + self.sensor_type = sensor_type + self.sensor_type_name = sensor_type_to_name(sensor_type) + self.sensors = sensors + self.sensor_parameters = sensor_parameters + self.sleep_period = sleep_period + self.hciLock = hciLock + self.daemon = True + self.start() + + def run(self): + print_line('Worker for {} sensors started'.format(self.sensor_type_name), sd_notify=True) + # Sensor data retrieving and publishing + while True: + with self.hciLock: + pool_sensors(self.sensor_type, self.sensors, self.sensor_parameters) + + if daemon_enabled: + print_line('Sleeping for {} ({} seconds) ...'.format(self.sensor_type_name, self.sleep_period)) + print() + sleep(self.sleep_period) + else: + break + + print_line('Execution finished for {}'.format(self.sensor_type_name), sd_notify=True) + print() + # Load configuration file config_dir = parse_args.config_dir @@ -476,10 +510,11 @@ def pool_sensors(sensor_type, sensors, parameters): mqtt_client.publish('{}/moisture/meta/type'.format(topic_path), 'rel_humidity', 1, True) mqtt_client.publish('{}/temperature/meta/type'.format(topic_path), 'temperature', 1, True) mqtt_client.publish('{}/timestamp/meta/type'.format(topic_path), 'text', 1, True) + sleep(0.5) # some slack for the publish roundtrip and callback function for [mitempbt_name, mitempbt] in mitempbts.items(): mqtt_client.publish('/devices/{}/meta/name'.format(mitempbt_name), mitempbt_name, 1, True) - topic_path = '/device s/{}/controls'.format(mitempbt_name) + topic_path = '/devices/{}/controls'.format(mitempbt_name) mqtt_client.publish('{}/battery/meta/type'.format(topic_path), 'value', 1, True) mqtt_client.publish('{}/battery/meta/units'.format(topic_path), '%', 1, True) mqtt_client.publish('{}/humidity/meta/type'.format(topic_path), 'rel_humidity', 1, True) @@ -490,44 +525,14 @@ def pool_sensors(sensor_type, sensors, parameters): print_line('Initialization complete, starting MQTT publish loop', console=False, sd_notify=True) - -class sensorPooler(threading.Thread): - def __init__(self, sensor_type, sensors, sensor_parameters, sleep_period): - threading.Thread.__init__(self) - self.sensor_type = sensor_type - self.sensors = sensors - self.sensor_parameters = sensor_parameters - self.sleep_period = sleep_period - self.daemon = True - def run(self): - sensor_type_name = sensor_type_to_name(self.sensor_type) - print_line('Worker for {} sensors started'.format(sensor_type_name), sd_notify=True) - # Sensor data retrieving and publishing - while True: - hciLock.acquire() - pool_sensors(self.sensor_type, self.sensors, self.sensor_parameters) - hciLock.release() - if daemon_enabled: - print_line('Sleeping for {} ({} seconds) ...'.format(sensor_type_name, self.sleep_period)) - print() - sleep(self.sleep_period) - else: - print_line('Execution finished for {} in non-daemon-mode'.format(sensor_type_name), sd_notify=True) - print() - break - hciLock = threading.Lock() threads = [] if len(mifloras) != 0: - mifloraThread = sensorPooler(sensor_type_miflora, mifloras, miflora_parameters, miflora_sleep_period) - mifloraThread.start() - threads.append(mifloraThread) + threads.append(sensorPooler(sensor_type_miflora, mifloras, miflora_parameters, miflora_sleep_period, hciLock)) if len(mitempbts) != 0: - mitempbtThread = sensorPooler(sensor_type_mitempbt, mitempbts, mitempbt_parameters, mitempbt_sleep_period) - mitempbtThread.start() - threads.append(mitempbtThread) + threads.append(sensorPooler(sensor_type_mitempbt, mitempbts, mitempbt_parameters, mitempbt_sleep_period, hciLock)) for thread in threads: thread.join() From f4ddf8429a82f7f01dafd424d4f213e551baf5d6 Mon Sep 17 00:00:00 2001 From: wojciej <3244472+wojciej@users.noreply.github.com> Date: Tue, 19 Feb 2019 18:46:24 +0100 Subject: [PATCH 15/15] Fixing default base_topic to misensor based on implementation Hey, thanks for your great work - I have started using this branch and I very much like it. However I would like propose update of config.ini.dist: In config.ini.dist there is the following information: # The MQTT base topic to publish all Mi Flora sensor data topics under. # Default depends on the configured reporting_method #base_topic = miflora # Default for: mqtt-json, mqtt-smarthome #base_topic = homie # Default for: mqtt-homie #base_topic = homeassistant # Default for: homeassistant-mqtt #base_topic = v1/devices/me/telemetry # Default for: thingsboard-json #base_topic = # Default for: wirenboard-mqtt So as log as I use mqtt-json the default toppic should be miflora. While the default toppic is no longer **miflora** but **misensor** - what makes total sense cause we no longer use only miflora sensors - also it was already implemented in miflora-mqtt-daemon.py: if reporting_mode == 'mqtt-homie': default_base_topic = 'homie' elif reporting_mode == 'homeassistant-mqtt': default_base_topic = 'homeassistant' elif reporting_mode == 'thingsboard-json': default_base_topic = 'v1/devices/me/telemetry' elif reporting_mode == 'wirenboard-mqtt': default_base_topic = '' else: default_base_topic = 'misensor' Hope you dont mind my changes --- config.ini.dist | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config.ini.dist b/config.ini.dist index 7a5a7fd..db8e83d 100644 --- a/config.ini.dist +++ b/config.ini.dist @@ -50,7 +50,7 @@ # The MQTT base topic to publish all Mi Flora sensor data topics under. # Default depends on the configured reporting_method -#base_topic = miflora # Default for: mqtt-json, mqtt-smarthome +#base_topic = misensor # Default for: mqtt-json, mqtt-smarthome #base_topic = homie # Default for: mqtt-homie #base_topic = homeassistant # Default for: homeassistant-mqtt #base_topic = v1/devices/me/telemetry # Default for: thingsboard-json @@ -90,4 +90,4 @@ [MiTempBt] -# Add your Mi Temerature & Humidity sensors here. Setup is same as for [MiFlora] section \ No newline at end of file +# Add your Mi Temerature & Humidity sensors here. Setup is same as for [MiFlora] section