Skip to content

Commit

Permalink
* added separate setting "period" per device type in config.ini
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
aqualx committed Jan 30, 2019
1 parent 4c41adf commit 51ad7d6
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 57 deletions.
10 changes: 7 additions & 3 deletions config.ini.dist
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,19 @@
#
#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]

# 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]

Expand Down Expand Up @@ -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
145 changes: 92 additions & 53 deletions miflora-mqtt-daemon.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")),
Expand Down Expand Up @@ -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()
Expand All @@ -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('%', '%%'))
Expand All @@ -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():
Expand All @@ -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}
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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']}
Expand All @@ -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)
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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)
Expand All @@ -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
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()
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
btlewrap==0.0.3
mithermometer==0.1.2

0 comments on commit 51ad7d6

Please sign in to comment.