miflora-mqtt-daemon.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env python3
  2. import sys
  3. import json
  4. import os.path
  5. from time import sleep, localtime, strftime
  6. from configparser import ConfigParser
  7. from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE
  8. import paho.mqtt.client as mqtt
  9. import sdnotify
  10. parameters = [MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE]
  11. # Intro
  12. print('Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon')
  13. print('Source: https://github.com/ThomDietrich/miflora-mqtt-daemon')
  14. print()
  15. # Systemd Service Notifications - https://github.com/bb4242/sdnotify
  16. sd_notifier = sdnotify.SystemdNotifier()
  17. # Eclipse Paho callbacks - http://www.eclipse.org/paho/clients/python/docs/#callbacks
  18. def on_connect(client, userdata, flags, rc):
  19. if rc == 0:
  20. print('Connected.\n')
  21. sd_notifier.notify('STATUS=MQTT connection established')
  22. else:
  23. print('Connection error with result code {} - {}'.format(str(rc), mqtt.connack_string(rc)), file=sys.stderr)
  24. #kill main thread
  25. os._exit(1)
  26. def on_publish(client, userdata, mid):
  27. print('Data successfully published.')
  28. # Load configuration file
  29. config = ConfigParser(delimiters=('=', ))
  30. config.optionxform = str
  31. config.read([os.path.join(sys.path[0], 'config.ini'), os.path.join(sys.path[0], 'config.local.ini')])
  32. reporting_mode = config['General'].get('reporting_method', 'mqtt-json')
  33. daemon_enabled = config['Daemon'].getboolean('enabled', True)
  34. topic_prefix = config['MQTT'].get('topic_prefix', 'miflora')
  35. sleep_period = config['Daemon'].getint('period', 300)
  36. #miflora_cache_timeout = config['MiFlora'].getint('cache_timeout', 600)
  37. miflora_cache_timeout = sleep_period - 1
  38. # Check configuration
  39. if not reporting_mode in ['mqtt-json', 'json']:
  40. print('Error. Configuration parameter reporting_mode set to an invalid value.', file=sys.stderr)
  41. sd_notifier.notify('STATUS=Configuration parameter reporting_mode set to an invalid value')
  42. sys.exit(1)
  43. if not config['Sensors']:
  44. print('Error. Please add at least one sensor to the configuration file "config.ini".', file=sys.stderr)
  45. print('Scan for available Miflora sensors with "sudo hcitool lescan".', file=sys.stderr)
  46. sd_notifier.notify('STATUS=No sensors found in configuration file "config.ini"')
  47. sys.exit(1)
  48. sd_notifier.notify('STATUS=Configuration accepted')
  49. # MQTT connection
  50. if reporting_mode == 'mqtt-json':
  51. print('Connecting to MQTT broker ...')
  52. mqtt_client = mqtt.Client()
  53. mqtt_client.on_connect = on_connect
  54. mqtt_client.on_publish = on_publish
  55. mqtt_client.will_set('{}/$announce'.format(topic_prefix), payload='{}', retain=True)
  56. if config['MQTT'].get('username'):
  57. mqtt_client.username_pw_set(config['MQTT'].get('username'), config['MQTT'].get('password', None))
  58. try:
  59. mqtt_client.connect(config['MQTT'].get('hostname', 'localhost'),
  60. port=config['MQTT'].getint('port', 1883),
  61. keepalive=config['MQTT'].getint('keepalive', 60))
  62. except:
  63. print('Error. Please check your MQTT connection settings in the configuration file "config.ini".', file=sys.stderr)
  64. sd_notifier.notify('STATUS=Please check your MQTT connection settings in the configuration file "config.ini"')
  65. sys.exit(1)
  66. else:
  67. mqtt_client.loop_start()
  68. sleep(1.0) # some slack to establish the connection
  69. sd_notifier.notify('READY=1')
  70. # Initialize Mi Flora sensors
  71. flores = dict()
  72. for [name, mac] in config['Sensors'].items():
  73. sd_notifier.notify('STATUS=Attempting initial connection to MiFlora sensor "{}" ({})'.format(name, mac))
  74. print('Adding sensor to device list and testing connection...')
  75. print('Name: "{}"'.format(name))
  76. flora_poller = MiFloraPoller(mac=mac, cache_timeout=miflora_cache_timeout, retries=9)
  77. try:
  78. flora_poller.fill_cache()
  79. flora_poller.parameter_value(MI_LIGHT)
  80. except IOError:
  81. print('Error. Initial connection to Mi Flora sensor "{}" ({}) failed. Please check your setup and the MAC address.'.format(name, mac), file=sys.stderr)
  82. sd_notifier.notify('STATUS=Initial connection to Mi Flora sensor "{}" ({}) failed'.format(name, mac))
  83. sys.exit(1)
  84. print('Device name: "{}"'.format(flora_poller.name()))
  85. print('MAC address: {}'.format(flora_poller._mac))
  86. print('Firmware: {}'.format(flora_poller.firmware_version()))
  87. print()
  88. flora = dict()
  89. flora['mac'] = flora_poller._mac
  90. flora['poller'] = flora_poller
  91. flora['firmware'] = flora_poller.firmware_version()
  92. flora['refresh'] = sleep_period
  93. flora['location'] = ''
  94. flores[name] = flora
  95. # Discovery Announcement
  96. if reporting_mode == 'mqtt-json':
  97. print('Announcing MiFlora devices to MQTT broker for auto-discovery ...')
  98. flores_info = dict()
  99. for [flora_name, flora] in flores.items():
  100. flora_info = {key: value for key, value in flora.items() if key not in ['poller']}
  101. flora_info['topic'] = '{}/{}'.format(topic_prefix, flora_name)
  102. flores_info[flora_name] = flora_info
  103. mqtt_client.publish('{}/$announce'.format(topic_prefix), json.dumps(flores_info), retain=True)
  104. sleep(0.5) # some slack for the publish roundtrip and callback function
  105. print()
  106. sd_notifier.notify('STATUS=Initialization complete, starting MQTT publish loop')
  107. # Sensor data retrieval and publication
  108. while True:
  109. for [flora_name, flora] in flores.items():
  110. data = dict()
  111. while not flora['poller']._cache:
  112. try:
  113. flora['poller'].fill_cache()
  114. except IOError:
  115. print('Failed to retrieve data from Mi Flora Sensor "{}" ({}). Retrying ...'.format(flora_name, flora['mac']), file=sys.stderr)
  116. sd_notifier.notify('STATUS=Failed to retrieve data from Mi Flora Sensor "{}" ({}). Retrying ...'.format(flora_name, flora['mac']))
  117. for param in parameters:
  118. data[param] = flora['poller'].parameter_value(param)
  119. timestamp = strftime('%Y-%m-%d %H:%M:%S', localtime())
  120. if reporting_mode == 'mqtt-json':
  121. print('[{}] Attempting to publishing to MQTT topic "{}/{}" ...\nData: {}'.format(timestamp, topic_prefix, flora_name, json.dumps(data)))
  122. mqtt_client.publish('{}/{}'.format(topic_prefix, flora_name), json.dumps(data))
  123. sleep(0.5) # some slack for the publish roundtrip and callback function
  124. print()
  125. elif reporting_mode == 'json':
  126. data['timestamp'] = timestamp
  127. data['name'] = flora_name
  128. data['mac'] = flora['mac']
  129. data['firmware'] = flora['firmware']
  130. print('Data:', json.dumps(data))
  131. else:
  132. raise NameError('Unexpected reporting_mode.')
  133. sd_notifier.notify('STATUS={} - Status messages for all sensors published'.format(strftime('%Y-%m-%d %H:%M:%S', localtime())))
  134. if daemon_enabled:
  135. print('Sleeping ({} seconds) ...'.format(sleep_period))
  136. sleep(sleep_period)
  137. print()
  138. else:
  139. print('Execution finished in non-daemon-mode.')
  140. sd_notifier.notify('STATUS=Execution finished in non-daemon-mode')
  141. if reporting_mode == 'mqtt-json':
  142. mqtt_client.disconnect()
  143. break