2017-02-09 21:15:25 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2017-07-24 10:41:39 +02:00
|
|
|
import sys
|
2017-07-30 15:27:08 +02:00
|
|
|
import re
|
2017-02-09 21:15:25 +01:00
|
|
|
import json
|
2017-07-25 00:02:18 +02:00
|
|
|
import os.path
|
2017-07-24 10:41:39 +02:00
|
|
|
from time import sleep, localtime, strftime
|
|
|
|
from configparser import ConfigParser
|
|
|
|
from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE
|
|
|
|
import paho.mqtt.client as mqtt
|
2017-07-26 02:17:20 +02:00
|
|
|
import sdnotify
|
2017-07-24 10:41:39 +02:00
|
|
|
|
|
|
|
parameters = [MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE]
|
2017-02-09 21:15:25 +01:00
|
|
|
|
2017-07-26 00:19:26 +02:00
|
|
|
# Intro
|
2017-07-24 10:41:39 +02:00
|
|
|
print('Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon')
|
2017-07-26 00:19:26 +02:00
|
|
|
print('Source: https://github.com/ThomDietrich/miflora-mqtt-daemon')
|
2017-07-24 10:41:39 +02:00
|
|
|
print()
|
2017-02-09 21:15:25 +01:00
|
|
|
|
2017-07-30 15:27:08 +02:00
|
|
|
if False:
|
|
|
|
print('Sorry, this script requires a python3 runtime environemt.', file=sys.stderr)
|
|
|
|
|
2017-07-26 02:17:20 +02:00
|
|
|
# Systemd Service Notifications - https://github.com/bb4242/sdnotify
|
|
|
|
sd_notifier = sdnotify.SystemdNotifier()
|
|
|
|
|
|
|
|
# Eclipse Paho callbacks - http://www.eclipse.org/paho/clients/python/docs/#callbacks
|
2017-07-25 17:17:20 +02:00
|
|
|
def on_connect(client, userdata, flags, rc):
|
|
|
|
if rc == 0:
|
|
|
|
print('Connected.\n')
|
2017-07-26 02:17:20 +02:00
|
|
|
sd_notifier.notify('STATUS=MQTT connection established')
|
2017-07-25 17:17:20 +02:00
|
|
|
else:
|
|
|
|
print('Connection error with result code {} - {}'.format(str(rc), mqtt.connack_string(rc)), file=sys.stderr)
|
|
|
|
#kill main thread
|
|
|
|
os._exit(1)
|
|
|
|
|
|
|
|
def on_publish(client, userdata, mid):
|
2017-07-28 02:39:20 +02:00
|
|
|
#print('Data successfully published.')
|
|
|
|
pass
|
2017-07-25 17:17:20 +02:00
|
|
|
|
2017-07-30 19:13:57 +02:00
|
|
|
def flores_to_openhab_items(flores):
|
|
|
|
items = list()
|
|
|
|
items.append('// Generated by miflora-mqtt-daemon. Adapt to your needs!\n\n// Mi Flora specific groups')
|
|
|
|
for param in parameters:
|
|
|
|
items.append('Group g{} "Mi Flora{} elements" (gAll)'.format(param.capitalize(), param.capitalize()))
|
|
|
|
for [flora_name, flora] in flores.items():
|
|
|
|
items.append('\n// Mi Flora "{}" ({})'.format(flora['pretty'], flora['mac']))
|
|
|
|
for param in parameters:
|
|
|
|
basic = 'Number {}_{}'.format(flora['location'], flora_name.capitalize())
|
|
|
|
parameter = '"{} {} {} [%.0f]" <text> (g{}, g{})'.format(flora['location'], flora['pretty'], param.capitalize(), flora['location'], param.capitalize())
|
|
|
|
channel = '{{mqtt="<[broker:{}/{}:state:JSONPATH($.{})]"}}'.format(base_topic, flora_name, param)
|
|
|
|
items.append(' '.join([basic, parameter, channel]))
|
|
|
|
print('\n'.join(items))
|
|
|
|
|
2017-07-24 10:41:39 +02:00
|
|
|
# Load configuration file
|
2017-02-09 21:15:25 +01:00
|
|
|
config = ConfigParser(delimiters=('=', ))
|
2017-07-24 10:41:39 +02:00
|
|
|
config.optionxform = str
|
2017-07-26 02:17:20 +02:00
|
|
|
config.read([os.path.join(sys.path[0], 'config.ini'), os.path.join(sys.path[0], 'config.local.ini')])
|
2017-07-25 17:17:20 +02:00
|
|
|
|
|
|
|
reporting_mode = config['General'].get('reporting_method', 'mqtt-json')
|
2017-07-24 10:41:39 +02:00
|
|
|
daemon_enabled = config['Daemon'].getboolean('enabled', True)
|
2017-07-28 02:39:20 +02:00
|
|
|
base_topic = config['MQTT'].get('base_topic', 'homie' if reporting_mode == 'mqtt-homie' else 'miflora')
|
|
|
|
device_id = config['MQTT'].get('homie_device_id', 'miflora-mqtt-daemon')
|
2017-07-26 00:19:26 +02:00
|
|
|
sleep_period = config['Daemon'].getint('period', 300)
|
|
|
|
#miflora_cache_timeout = config['MiFlora'].getint('cache_timeout', 600)
|
|
|
|
miflora_cache_timeout = sleep_period - 1
|
2017-07-25 17:17:20 +02:00
|
|
|
|
2017-07-26 00:19:26 +02:00
|
|
|
# Check configuration
|
2017-07-28 02:39:20 +02:00
|
|
|
if not reporting_mode in ['mqtt-json', 'mqtt-homie', 'json']:
|
2017-07-25 17:17:20 +02:00
|
|
|
print('Error. Configuration parameter reporting_mode set to an invalid value.', file=sys.stderr)
|
2017-07-27 00:21:15 +02:00
|
|
|
sd_notifier.notify('STATUS=Configuration parameter reporting_mode set to an invalid value')
|
2017-07-25 17:17:20 +02:00
|
|
|
sys.exit(1)
|
2017-07-24 10:41:39 +02:00
|
|
|
if not config['Sensors']:
|
|
|
|
print('Error. Please add at least one sensor to the configuration file "config.ini".', file=sys.stderr)
|
2017-07-26 00:19:26 +02:00
|
|
|
print('Scan for available Miflora sensors with "sudo hcitool lescan".', file=sys.stderr)
|
2017-07-27 00:21:15 +02:00
|
|
|
sd_notifier.notify('STATUS=No sensors found in configuration file "config.ini"')
|
2017-07-24 10:41:39 +02:00
|
|
|
sys.exit(1)
|
2017-07-26 02:17:20 +02:00
|
|
|
sd_notifier.notify('STATUS=Configuration accepted')
|
2017-02-09 21:15:25 +01:00
|
|
|
|
2017-07-25 17:17:20 +02:00
|
|
|
# MQTT connection
|
2017-07-28 02:39:20 +02:00
|
|
|
if reporting_mode in ['mqtt-json', 'mqtt-homie']:
|
2017-07-25 17:17:20 +02:00
|
|
|
print('Connecting to MQTT broker ...')
|
|
|
|
mqtt_client = mqtt.Client()
|
|
|
|
mqtt_client.on_connect = on_connect
|
|
|
|
mqtt_client.on_publish = on_publish
|
2017-07-28 02:39:20 +02:00
|
|
|
if reporting_mode == 'mqtt-json':
|
|
|
|
mqtt_client.will_set('{}/$announce'.format(base_topic), payload='{}', retain=True)
|
|
|
|
elif reporting_mode == 'mqtt-homie':
|
|
|
|
mqtt_client.will_set('{}/{}/$online'.format(base_topic, device_id), payload='false', retain=True)
|
2017-07-25 17:17:20 +02:00
|
|
|
if config['MQTT'].get('username'):
|
|
|
|
mqtt_client.username_pw_set(config['MQTT'].get('username'), config['MQTT'].get('password', None))
|
|
|
|
try:
|
|
|
|
mqtt_client.connect(config['MQTT'].get('hostname', 'localhost'),
|
|
|
|
port=config['MQTT'].getint('port', 1883),
|
|
|
|
keepalive=config['MQTT'].getint('keepalive', 60))
|
|
|
|
except:
|
|
|
|
print('Error. Please check your MQTT connection settings in the configuration file "config.ini".', file=sys.stderr)
|
2017-07-27 00:21:15 +02:00
|
|
|
sd_notifier.notify('STATUS=Please check your MQTT connection settings in the configuration file "config.ini"')
|
2017-07-25 17:17:20 +02:00
|
|
|
sys.exit(1)
|
|
|
|
else:
|
|
|
|
mqtt_client.loop_start()
|
2017-07-27 22:11:56 +02:00
|
|
|
sleep(1.0) # some slack to establish the connection
|
2017-07-25 17:17:20 +02:00
|
|
|
|
2017-07-26 02:17:20 +02:00
|
|
|
sd_notifier.notify('READY=1')
|
|
|
|
|
2017-07-24 10:41:39 +02:00
|
|
|
# Initialize Mi Flora sensors
|
2017-07-26 00:19:26 +02:00
|
|
|
flores = dict()
|
|
|
|
for [name, mac] in config['Sensors'].items():
|
2017-07-28 03:06:33 +02:00
|
|
|
location = ''
|
|
|
|
if '@' in name:
|
|
|
|
name, location = name.split("@")
|
|
|
|
if not re.match("C4:7C:8D:[0-9A-F]{2}:[0-9A-F]{2}:[0-9A-F]{2}", mac):
|
|
|
|
print('Error. The MAC address "{}" seems to be in the wrong format. Please check your configuration.'.format(mac), file=sys.stderr)
|
|
|
|
sd_notifier.notify('STATUS=The MAC address "{}" seems to be in the wrong format. Please check your configuration.'.format(mac))
|
|
|
|
sys.exit(1)
|
2017-07-28 02:39:20 +02:00
|
|
|
flora = dict()
|
|
|
|
print('Adding sensor to device list and testing connection ...')
|
2017-07-26 00:19:26 +02:00
|
|
|
print('Name: "{}"'.format(name))
|
2017-07-28 14:04:55 +02:00
|
|
|
sd_notifier.notify('STATUS=Attempting initial connection to Mi Flora sensor "{}" ({})'.format(name, mac))
|
2017-07-26 00:19:26 +02:00
|
|
|
flora_poller = MiFloraPoller(mac=mac, cache_timeout=miflora_cache_timeout, retries=9)
|
2017-07-28 02:39:20 +02:00
|
|
|
flora['poller'] = flora_poller
|
|
|
|
flora['mac'] = flora_poller._mac
|
2017-07-28 03:06:33 +02:00
|
|
|
flora['refresh'] = sleep_period
|
|
|
|
flora['location'] = location
|
2017-07-27 00:21:15 +02:00
|
|
|
try:
|
2017-07-27 21:51:38 +02:00
|
|
|
flora_poller.fill_cache()
|
2017-07-27 00:21:15 +02:00
|
|
|
flora_poller.parameter_value(MI_LIGHT)
|
2017-07-28 02:39:20 +02:00
|
|
|
flora['firmware'] = flora_poller.firmware_version()
|
2017-07-27 00:21:15 +02:00
|
|
|
except IOError:
|
2017-07-27 21:51:38 +02:00
|
|
|
print('Error. Initial connection to Mi Flora sensor "{}" ({}) failed. Please check your setup and the MAC address.'.format(name, mac), file=sys.stderr)
|
|
|
|
sd_notifier.notify('STATUS=Initial connection to Mi Flora sensor "{}" ({}) failed'.format(name, mac))
|
2017-07-28 02:39:20 +02:00
|
|
|
continue
|
|
|
|
else:
|
|
|
|
print('Device name: "{}"'.format(flora_poller.name()))
|
|
|
|
print('MAC address: {}'.format(flora_poller._mac))
|
|
|
|
print('Firmware: {}'.format(flora_poller.firmware_version()))
|
|
|
|
print()
|
2017-07-27 22:39:49 +02:00
|
|
|
flores[name] = flora
|
|
|
|
|
2017-07-27 00:49:57 +02:00
|
|
|
# Discovery Announcement
|
2017-07-27 22:11:56 +02:00
|
|
|
if reporting_mode == 'mqtt-json':
|
2017-07-28 14:04:55 +02:00
|
|
|
print('Announcing Mi Flora devices to MQTT broker for auto-discovery ...')
|
2017-07-27 22:11:56 +02:00
|
|
|
flores_info = dict()
|
2017-07-27 22:39:49 +02:00
|
|
|
for [flora_name, flora] in flores.items():
|
|
|
|
flora_info = {key: value for key, value in flora.items() if key not in ['poller']}
|
2017-07-28 02:39:20 +02:00
|
|
|
flora_info['topic'] = '{}/{}'.format(base_topic, flora_name)
|
2017-07-27 22:11:56 +02:00
|
|
|
flores_info[flora_name] = flora_info
|
2017-07-28 02:39:20 +02:00
|
|
|
mqtt_client.publish('{}/$announce'.format(base_topic), json.dumps(flores_info), retain=True)
|
|
|
|
sleep(0.5) # some slack for the publish roundtrip and callback function
|
|
|
|
print()
|
|
|
|
elif reporting_mode == 'mqtt-homie':
|
2017-07-28 14:04:55 +02:00
|
|
|
print('Announcing Mi Flora devices to MQTT broker for auto-discovery ...')
|
2017-07-28 02:39:20 +02:00
|
|
|
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)
|
|
|
|
|
|
|
|
nodes_list = ','.join([flora_name for [flora_name, flora] in flores.items()])
|
|
|
|
mqtt_client.publish('{}/{}/$nodes'.format(base_topic, device_id), nodes_list, 1, True)
|
|
|
|
|
|
|
|
for [flora_name, flora] in flores.items():
|
|
|
|
mqtt_client.publish('{}/{}/{}/$type'.format(base_topic, device_id, flora_name), 'miflora', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/$properties'.format(base_topic, device_id, flora_name), 'battery,conductivity,light,moisture,temperature', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/battery/$settable'.format(base_topic, device_id, flora_name), 'false', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/battery/$unit'.format(base_topic, device_id, flora_name), 'percent', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/battery/$datatype'.format(base_topic, device_id, flora_name), 'int', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/battery/$range'.format(base_topic, device_id, flora_name), '0:100', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/conductivity/$settable'.format(base_topic, device_id, flora_name), 'false', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/conductivity/$unit'.format(base_topic, device_id, flora_name), 'µS/cm', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/conductivity/$datatype'.format(base_topic, device_id, flora_name), 'int', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/conductivity/$range'.format(base_topic, device_id, flora_name), '0:*', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/light/$settable'.format(base_topic, device_id, flora_name), 'false', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/light/$unit'.format(base_topic, device_id, flora_name), 'lux', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/light/$datatype'.format(base_topic, device_id, flora_name), 'int', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/light/$range'.format(base_topic, device_id, flora_name), '0:50000', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/moisture/$settable'.format(base_topic, device_id, flora_name), 'false', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/moisture/$unit'.format(base_topic, device_id, flora_name), 'percent', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/moisture/$datatype'.format(base_topic, device_id, flora_name), 'int', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/moisture/$range'.format(base_topic, device_id, flora_name), '0:100', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/temperature/$settable'.format(base_topic, device_id, flora_name), 'false', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/temperature/$unit'.format(base_topic, device_id, flora_name), '°C', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/temperature/$datatype'.format(base_topic, device_id, flora_name), 'float', 1, True)
|
|
|
|
mqtt_client.publish('{}/{}/{}/temperature/$range'.format(base_topic, device_id, flora_name), '*', 1, True)
|
2017-07-27 22:11:56 +02:00
|
|
|
sleep(0.5) # some slack for the publish roundtrip and callback function
|
|
|
|
print()
|
2017-07-27 00:49:57 +02:00
|
|
|
|
2017-07-26 02:17:20 +02:00
|
|
|
sd_notifier.notify('STATUS=Initialization complete, starting MQTT publish loop')
|
|
|
|
|
2017-07-25 17:17:20 +02:00
|
|
|
# Sensor data retrieval and publication
|
2017-02-09 21:15:25 +01:00
|
|
|
while True:
|
2017-07-27 22:39:49 +02:00
|
|
|
for [flora_name, flora] in flores.items():
|
2017-07-26 00:19:26 +02:00
|
|
|
data = dict()
|
2017-07-28 02:39:20 +02:00
|
|
|
retries = 3
|
|
|
|
while retries > 0 and not flora['poller']._cache:
|
2017-07-27 21:51:38 +02:00
|
|
|
try:
|
2017-07-27 22:39:49 +02:00
|
|
|
flora['poller'].fill_cache()
|
2017-07-30 15:27:08 +02:00
|
|
|
flora['poller'].parameter_value(MI_LIGHT)
|
2017-07-27 21:51:38 +02:00
|
|
|
except IOError:
|
2017-07-27 22:39:49 +02:00
|
|
|
print('Failed to retrieve data from Mi Flora Sensor "{}" ({}). Retrying ...'.format(flora_name, flora['mac']), file=sys.stderr)
|
|
|
|
sd_notifier.notify('STATUS=Failed to retrieve data from Mi Flora Sensor "{}" ({}). Retrying ...'.format(flora_name, flora['mac']))
|
2017-07-28 02:39:20 +02:00
|
|
|
retries = retries - 1
|
|
|
|
if not flora['poller']._cache:
|
|
|
|
continue
|
2017-02-09 21:15:25 +01:00
|
|
|
for param in parameters:
|
2017-07-27 22:39:49 +02:00
|
|
|
data[param] = flora['poller'].parameter_value(param)
|
2017-07-27 21:51:38 +02:00
|
|
|
|
2017-07-25 17:17:20 +02:00
|
|
|
timestamp = strftime('%Y-%m-%d %H:%M:%S', localtime())
|
|
|
|
|
|
|
|
if reporting_mode == 'mqtt-json':
|
2017-07-28 02:39:20 +02:00
|
|
|
print('[{}] Attempting to publishing to MQTT topic "{}/{}" ...\nData: {}'.format(timestamp, base_topic, flora_name, json.dumps(data)))
|
|
|
|
mqtt_client.publish('{}/{}'.format(base_topic, flora_name), json.dumps(data))
|
|
|
|
sleep(0.5) # some slack for the publish roundtrip and callback function
|
|
|
|
print()
|
|
|
|
elif reporting_mode == 'mqtt-homie':
|
|
|
|
print('[{}] Attempting to publishing data for Mi Flora "{}" ...\nData: {}'.format(timestamp, flora_name, str(data)))
|
|
|
|
for [param, value] in data.items():
|
|
|
|
mqtt_client.publish('{}/{}/{}/{}'.format(base_topic, device_id, flora_name, param), value, 1, False)
|
2017-07-25 17:17:20 +02:00
|
|
|
sleep(0.5) # some slack for the publish roundtrip and callback function
|
|
|
|
print()
|
|
|
|
elif reporting_mode == 'json':
|
|
|
|
data['timestamp'] = timestamp
|
2017-07-26 00:19:26 +02:00
|
|
|
data['name'] = flora_name
|
2017-07-27 22:39:49 +02:00
|
|
|
data['mac'] = flora['mac']
|
|
|
|
data['firmware'] = flora['firmware']
|
2017-07-26 00:19:26 +02:00
|
|
|
print('Data:', json.dumps(data))
|
2017-07-25 17:17:20 +02:00
|
|
|
else:
|
|
|
|
raise NameError('Unexpected reporting_mode.')
|
|
|
|
|
2017-07-27 00:21:15 +02:00
|
|
|
sd_notifier.notify('STATUS={} - Status messages for all sensors published'.format(strftime('%Y-%m-%d %H:%M:%S', localtime())))
|
2017-07-26 02:17:20 +02:00
|
|
|
|
2017-07-26 00:19:26 +02:00
|
|
|
if daemon_enabled:
|
2017-07-25 17:17:20 +02:00
|
|
|
print('Sleeping ({} seconds) ...'.format(sleep_period))
|
|
|
|
sleep(sleep_period)
|
|
|
|
print()
|
2017-07-26 00:19:26 +02:00
|
|
|
else:
|
2017-07-27 22:11:56 +02:00
|
|
|
print('Execution finished in non-daemon-mode.')
|
2017-07-27 00:21:15 +02:00
|
|
|
sd_notifier.notify('STATUS=Execution finished in non-daemon-mode')
|
2017-07-26 02:17:20 +02:00
|
|
|
if reporting_mode == 'mqtt-json':
|
|
|
|
mqtt_client.disconnect()
|
2017-07-26 00:19:26 +02:00
|
|
|
break
|
2017-02-09 21:15:25 +01:00
|
|
|
|