miflora-mqtt-daemon.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. #!/usr/bin/env python3
  2. import ssl
  3. import sys
  4. import re
  5. import json
  6. import os.path
  7. import argparse
  8. from time import time, sleep, localtime, strftime
  9. from collections import OrderedDict
  10. from colorama import init as colorama_init
  11. from colorama import Fore, Back, Style
  12. from configparser import ConfigParser
  13. from unidecode import unidecode
  14. from miflora.miflora_poller import MiFloraPoller, MI_BATTERY, MI_CONDUCTIVITY, MI_LIGHT, MI_MOISTURE, MI_TEMPERATURE
  15. from btlewrap import BluepyBackend, GatttoolBackend, BluetoothBackendException
  16. from bluepy.btle import BTLEException
  17. import paho.mqtt.client as mqtt
  18. import sdnotify
  19. from signal import signal, SIGPIPE, SIG_DFL
  20. signal(SIGPIPE,SIG_DFL)
  21. project_name = 'Xiaomi Mi Flora Plant Sensor MQTT Client/Daemon'
  22. project_url = 'https://github.com/ThomDietrich/miflora-mqtt-daemon'
  23. parameters = OrderedDict([
  24. (MI_LIGHT, dict(name="LightIntensity", name_pretty='Sunlight Intensity', typeformat='%d', unit='lux', device_class="illuminance", state_class="measurement")),
  25. (MI_TEMPERATURE, dict(name="AirTemperature", name_pretty='Air Temperature', typeformat='%.1f', unit='°C', device_class="temperature", state_class="measurement")),
  26. (MI_MOISTURE, dict(name="SoilMoisture", name_pretty='Soil Moisture', typeformat='%d', unit='%', device_class="humidity", state_class="measurement")),
  27. (MI_CONDUCTIVITY, dict(name="SoilConductivity", name_pretty='Soil Conductivity/Fertility', typeformat='%d', unit='µS/cm', state_class="measurement")),
  28. (MI_BATTERY, dict(name="Battery", name_pretty='Sensor Battery Level', typeformat='%d', unit='%', device_class="battery", state_class="measurement"))
  29. ])
  30. if False:
  31. # will be caught by python 2.7 to be illegal syntax
  32. print('Sorry, this script requires a python3 runtime environment.', file=sys.stderr)
  33. # Argparse
  34. parser = argparse.ArgumentParser(description=project_name, epilog='For further details see: ' + project_url)
  35. parser.add_argument('--config_dir', help='set directory where config.ini is located', default=sys.path[0])
  36. parse_args = parser.parse_args()
  37. # Intro
  38. colorama_init()
  39. print(Fore.GREEN + Style.BRIGHT)
  40. print(project_name)
  41. print('Source:', project_url)
  42. print(Style.RESET_ALL)
  43. # Systemd Service Notifications - https://github.com/bb4242/sdnotify
  44. sd_notifier = sdnotify.SystemdNotifier()
  45. # Logging function
  46. def print_line(text, error = False, warning=False, sd_notify=False, console=True):
  47. timestamp = strftime('%Y-%m-%d %H:%M:%S', localtime())
  48. if console:
  49. if error:
  50. print(Fore.RED + Style.BRIGHT + '[{}] '.format(timestamp) + Style.RESET_ALL + '{}'.format(text) + Style.RESET_ALL, file=sys.stderr)
  51. elif warning:
  52. print(Fore.YELLOW + '[{}] '.format(timestamp) + Style.RESET_ALL + '{}'.format(text) + Style.RESET_ALL)
  53. else:
  54. print(Fore.GREEN + '[{}] '.format(timestamp) + Style.RESET_ALL + '{}'.format(text) + Style.RESET_ALL)
  55. timestamp_sd = strftime('%b %d %H:%M:%S', localtime())
  56. if sd_notify:
  57. sd_notifier.notify('STATUS={} - {}.'.format(timestamp_sd, unidecode(text)))
  58. # Identifier cleanup
  59. def clean_identifier(name):
  60. clean = name.strip()
  61. for this, that in [[' ', '-'], ['ä', 'ae'], ['Ä', 'Ae'], ['ö', 'oe'], ['Ö', 'Oe'], ['ü', 'ue'], ['Ü', 'Ue'], ['ß', 'ss']]:
  62. clean = clean.replace(this, that)
  63. clean = unidecode(clean)
  64. return clean
  65. # Eclipse Paho callbacks - http://www.eclipse.org/paho/clients/python/docs/#callbacks
  66. def on_connect(client, userdata, flags, rc):
  67. if rc == 0:
  68. print_line('MQTT connection established', console=True, sd_notify=True)
  69. print()
  70. else:
  71. print_line('Connection error with result code {} - {}'.format(str(rc), mqtt.connack_string(rc)), error=True)
  72. #kill main thread
  73. os._exit(1)
  74. def on_publish(client, userdata, mid):
  75. #print_line('Data successfully published.')
  76. pass
  77. # Load configuration file
  78. config_dir = parse_args.config_dir
  79. config = ConfigParser(delimiters=('=', ), inline_comment_prefixes=('#'))
  80. config.optionxform = str
  81. try:
  82. with open(os.path.join(config_dir, 'config.ini')) as config_file:
  83. config.read_file(config_file)
  84. except IOError:
  85. print_line('No configuration file "config.ini"', error=True, sd_notify=True)
  86. sys.exit(1)
  87. reporting_mode = config['General'].get('reporting_method', 'mqtt-json')
  88. used_adapter = config['General'].get('adapter', 'hci0')
  89. daemon_enabled = config['Daemon'].getboolean('enabled', True)
  90. if reporting_mode == 'mqtt-homie':
  91. default_base_topic = 'homie'
  92. elif reporting_mode == 'homeassistant-mqtt':
  93. default_base_topic = 'homeassistant'
  94. elif reporting_mode == 'thingsboard-json':
  95. default_base_topic = 'v1/devices/me/telemetry'
  96. elif reporting_mode == 'wirenboard-mqtt':
  97. default_base_topic = ''
  98. else:
  99. default_base_topic = 'miflora'
  100. base_topic = config['MQTT'].get('base_topic', default_base_topic).lower()
  101. sleep_period = config['Daemon'].getint('period', 300)
  102. miflora_cache_timeout = sleep_period - 1
  103. # Check configuration
  104. if reporting_mode not in ['mqtt-json', 'mqtt-homie', 'json', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json', 'wirenboard-mqtt']:
  105. print_line('Configuration parameter reporting_mode set to an invalid value', error=True, sd_notify=True)
  106. sys.exit(1)
  107. if not config['Sensors']:
  108. print_line('No sensors found in configuration file "config.ini"', error=True, sd_notify=True)
  109. sys.exit(1)
  110. if reporting_mode == 'wirenboard-mqtt' and base_topic:
  111. print_line('Parameter "base_topic" ignored for "reporting_method = wirenboard-mqtt"', warning=True, sd_notify=True)
  112. print_line('Configuration accepted', console=False, sd_notify=True)
  113. # MQTT connection
  114. if reporting_mode in ['mqtt-json', 'mqtt-smarthome', 'homeassistant-mqtt', 'thingsboard-json', 'wirenboard-mqtt']:
  115. print_line('Connecting to MQTT broker ...')
  116. mqtt_client = mqtt.Client()
  117. mqtt_client.on_connect = on_connect
  118. mqtt_client.on_publish = on_publish
  119. if reporting_mode == 'mqtt-json':
  120. mqtt_client.will_set('{}/$announce'.format(base_topic), payload='{}', retain=True)
  121. elif reporting_mode == 'mqtt-smarthome':
  122. mqtt_client.will_set('{}/connected'.format(base_topic), payload='0', retain=True)
  123. if config['MQTT'].getboolean('tls', False):
  124. # According to the docs, setting PROTOCOL_SSLv23 "Selects the highest protocol version
  125. # that both the client and server support. Despite the name, this option can select
  126. # “TLS” protocols as well as “SSL”" - so this seems like a resonable default
  127. mqtt_client.tls_set(
  128. ca_certs=config['MQTT'].get('tls_ca_cert', None),
  129. keyfile=config['MQTT'].get('tls_keyfile', None),
  130. certfile=config['MQTT'].get('tls_certfile', None),
  131. tls_version=ssl.PROTOCOL_SSLv23
  132. )
  133. mqtt_username = os.environ.get("MQTT_USERNAME", config['MQTT'].get('username'))
  134. mqtt_password = os.environ.get("MQTT_PASSWORD", config['MQTT'].get('password', None))
  135. if mqtt_username:
  136. mqtt_client.username_pw_set(mqtt_username, mqtt_password)
  137. try:
  138. mqtt_client.connect(os.environ.get('MQTT_HOSTNAME', config['MQTT'].get('hostname', 'localhost')),
  139. port=int(os.environ.get('MQTT_PORT', config['MQTT'].get('port', '1883'))),
  140. keepalive=config['MQTT'].getint('keepalive', 60))
  141. except:
  142. print_line('MQTT connection error. Please check your settings in the configuration file "config.ini"', error=True, sd_notify=True)
  143. sys.exit(1)
  144. else:
  145. if reporting_mode == 'mqtt-smarthome':
  146. mqtt_client.publish('{}/connected'.format(base_topic), payload='1', retain=True)
  147. if reporting_mode != 'thingsboard-json':
  148. mqtt_client.loop_start()
  149. sleep(1.0) # some slack to establish the connection
  150. sd_notifier.notify('READY=1')
  151. # Initialize Mi Flora sensors
  152. flores = OrderedDict()
  153. for [name, mac] in config['Sensors'].items():
  154. if not re.match("[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}:[0-9a-f]{2}", mac.lower()):
  155. print_line('The MAC address "{}" seems to be in the wrong format. Please check your configuration'.format(mac), error=True, sd_notify=True)
  156. sys.exit(1)
  157. if '@' in name:
  158. name_pretty, location_pretty = name.split('@')
  159. else:
  160. name_pretty, location_pretty = name, ''
  161. name_clean = clean_identifier(name_pretty)
  162. location_clean = clean_identifier(location_pretty)
  163. flora = OrderedDict()
  164. print('Adding sensor to device list and testing connection ...')
  165. print('Name: "{}"'.format(name_pretty))
  166. # print_line('Attempting initial connection to Mi Flora sensor "{}" ({})'.format(name_pretty, mac), console=False, sd_notify=True)
  167. flora_poller = MiFloraPoller(mac=mac, backend=BluepyBackend, cache_timeout=miflora_cache_timeout, adapter=used_adapter)
  168. flora['poller'] = flora_poller
  169. flora['name_pretty'] = name_pretty
  170. flora['mac'] = flora_poller._mac
  171. flora['refresh'] = sleep_period
  172. flora['location_clean'] = location_clean
  173. flora['location_pretty'] = location_pretty
  174. flora['stats'] = {"count": 0, "success": 0, "failure": 0}
  175. flora['firmware'] = "0.0.0"
  176. try:
  177. flora_poller.fill_cache()
  178. flora_poller.parameter_value(MI_LIGHT)
  179. flora['firmware'] = flora_poller.firmware_version()
  180. except (IOError, BluetoothBackendException, BTLEException, RuntimeError, BrokenPipeError) as e:
  181. print_line('Initial connection to Mi Flora sensor "{}" ({}) failed due to exception: {}'.format(name_pretty, mac, e), error=True, sd_notify=True)
  182. else:
  183. print('Internal name: "{}"'.format(name_clean))
  184. print('Device name: "{}"'.format(flora_poller.name()))
  185. print('MAC address: {}'.format(flora_poller._mac))
  186. print('Firmware: {}'.format(flora_poller.firmware_version()))
  187. print_line('Initial connection to Mi Flora sensor "{}" ({}) successful'.format(name_pretty, mac), sd_notify=True)
  188. if int(flora_poller.firmware_version().replace(".", "")) < 319:
  189. print_line('Mi Flora sensor with a firmware version before 3.1.9 is not supported. Please update now.'.format(name_pretty, mac), error=True, sd_notify=True)
  190. print()
  191. flores[name_clean] = flora
  192. # Discovery Announcement
  193. if reporting_mode == 'mqtt-json':
  194. print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...')
  195. flores_info = dict()
  196. for [flora_name, flora] in flores.items():
  197. flora_info = {key: value for key, value in flora.items() if key not in ['poller', 'stats']}
  198. flora_info['topic'] = '{}/{}'.format(base_topic, flora_name)
  199. flores_info[flora_name] = flora_info
  200. mqtt_client.publish('{}/$announce'.format(base_topic), json.dumps(flores_info), retain=True)
  201. sleep(0.5) # some slack for the publish roundtrip and callback function
  202. print()
  203. elif reporting_mode == 'mqtt-homie':
  204. mqtt_client = OrderedDict()
  205. print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...')
  206. for [flora_name, flora] in flores.items():
  207. print_line('Connecting to MQTT broker for "{}" ...'.format(flora['name_pretty']))
  208. mqtt_client[flora_name.lower()] = mqtt.Client(flora_name.lower())
  209. mqtt_client[flora_name.lower()].on_connect = on_connect
  210. mqtt_client[flora_name.lower()].on_publish = on_publish
  211. mqtt_client[flora_name.lower()].will_set('{}/{}/$state'.format(base_topic, flora_name.lower()), payload='disconnected', retain=True)
  212. if config['MQTT'].getboolean('tls', False):
  213. # According to the docs, setting PROTOCOL_SSLv23 "Selects the highest protocol version
  214. # that both the client and server support. Despite the name, this option can select
  215. # “TLS” protocols as well as “SSL”" - so this seems like a resonable default
  216. mqtt_client[flora_name.lower()].tls_set(
  217. ca_certs=config['MQTT'].get('tls_ca_cert', None),
  218. keyfile=config['MQTT'].get('tls_keyfile', None),
  219. certfile=config['MQTT'].get('tls_certfile', None),
  220. tls_version=ssl.PROTOCOL_SSLv23
  221. )
  222. mqtt_username = os.environ.get("MQTT_USERNAME", config['MQTT'].get('username'))
  223. mqtt_password = os.environ.get("MQTT_PASSWORD", config['MQTT'].get('password', None))
  224. if mqtt_username:
  225. mqtt_client[flora_name.lower()].username_pw_set(mqtt_username, mqtt_password)
  226. try:
  227. mqtt_client[flora_name.lower()].connect(os.environ.get('MQTT_HOSTNAME', config['MQTT'].get('hostname', 'localhost')),
  228. port=int(os.environ.get('MQTT_PORT', config['MQTT'].get('port', '1883'))),
  229. keepalive=config['MQTT'].getint('keepalive', 60))
  230. except:
  231. print_line('MQTT connection error. Please check your settings in the configuration file "config.ini"', error=True, sd_notify=True)
  232. sys.exit(1)
  233. else:
  234. mqtt_client[flora_name.lower()].loop_start()
  235. sleep(1.0) # some slack to establish the connection
  236. topic_path = '{}/{}'.format(base_topic, flora_name.lower())
  237. mqtt_client[flora_name.lower()].publish('{}/$homie'.format(topic_path), '3.0', 1, True)
  238. mqtt_client[flora_name.lower()].publish('{}/$name'.format(topic_path), flora['name_pretty'], 1, True)
  239. mqtt_client[flora_name.lower()].publish('{}/$state'.format(topic_path), 'ready', 1, True)
  240. mqtt_client[flora_name.lower()].publish('{}/$mac'.format(topic_path), flora['mac'], 1, True)
  241. mqtt_client[flora_name.lower()].publish('{}/$stats'.format(topic_path), 'interval,timestamp', 1, True)
  242. mqtt_client[flora_name.lower()].publish('{}/$stats/interval'.format(topic_path), flora['refresh'], 1, True)
  243. mqtt_client[flora_name.lower()].publish('{}/$stats/timestamp'.format(topic_path), strftime('%Y-%m-%dT%H:%M:%S%z', localtime()), 1, True)
  244. mqtt_client[flora_name.lower()].publish('{}/$fw/name'.format(topic_path), 'miflora-firmware', 1, True)
  245. mqtt_client[flora_name.lower()].publish('{}/$fw/version'.format(topic_path), flora['firmware'], 1, True)
  246. mqtt_client[flora_name.lower()].publish('{}/$nodes'.format(topic_path), 'sensor', 1, True)
  247. sensor_path = '{}/sensor'.format(topic_path)
  248. mqtt_client[flora_name.lower()].publish('{}/$name'.format(sensor_path), 'miflora', 1, True)
  249. mqtt_client[flora_name.lower()].publish('{}/$properties'.format(sensor_path), 'battery,conductivity,light,moisture,temperature', 1, True)
  250. mqtt_client[flora_name.lower()].publish('{}/battery/$name'.format(sensor_path), 'battery', 1, True)
  251. mqtt_client[flora_name.lower()].publish('{}/battery/$settable'.format(sensor_path), 'false', 1, True)
  252. mqtt_client[flora_name.lower()].publish('{}/battery/$unit'.format(sensor_path), '%', 1, True)
  253. mqtt_client[flora_name.lower()].publish('{}/battery/$datatype'.format(sensor_path), 'integer', 1, True)
  254. mqtt_client[flora_name.lower()].publish('{}/battery/$format'.format(sensor_path), '0:100', 1, True)
  255. mqtt_client[flora_name.lower()].publish('{}/battery/$retained'.format(sensor_path), 'true', 1, True)
  256. mqtt_client[flora_name.lower()].publish('{}/conductivity/$name'.format(sensor_path), 'conductivity', 1, True)
  257. mqtt_client[flora_name.lower()].publish('{}/conductivity/$settable'.format(sensor_path), 'false', 1, True)
  258. mqtt_client[flora_name.lower()].publish('{}/conductivity/$unit'.format(sensor_path), 'µS/cm', 1, True)
  259. mqtt_client[flora_name.lower()].publish('{}/conductivity/$datatype'.format(sensor_path), 'integer', 1, True)
  260. mqtt_client[flora_name.lower()].publish('{}/conductivity/$format'.format(sensor_path), '0:*', 1, True)
  261. mqtt_client[flora_name.lower()].publish('{}/conductivity/$retained'.format(sensor_path), 'true', 1, True)
  262. mqtt_client[flora_name.lower()].publish('{}/light/$name'.format(sensor_path), 'light', 1, True)
  263. mqtt_client[flora_name.lower()].publish('{}/light/$settable'.format(sensor_path), 'false', 1, True)
  264. mqtt_client[flora_name.lower()].publish('{}/light/$unit'.format(sensor_path), 'lux', 1, True)
  265. mqtt_client[flora_name.lower()].publish('{}/light/$datatype'.format(sensor_path), 'integer', 1, True)
  266. mqtt_client[flora_name.lower()].publish('{}/light/$format'.format(sensor_path), '0:50000', 1, True)
  267. mqtt_client[flora_name.lower()].publish('{}/light/$retained'.format(sensor_path), 'true', 1, True)
  268. mqtt_client[flora_name.lower()].publish('{}/moisture/$name'.format(sensor_path), 'moisture', 1, True)
  269. mqtt_client[flora_name.lower()].publish('{}/moisture/$settable'.format(sensor_path), 'false', 1, True)
  270. mqtt_client[flora_name.lower()].publish('{}/moisture/$unit'.format(sensor_path), '%', 1, True)
  271. mqtt_client[flora_name.lower()].publish('{}/moisture/$datatype'.format(sensor_path), 'integer', 1, True)
  272. mqtt_client[flora_name.lower()].publish('{}/moisture/$format'.format(sensor_path), '0:100', 1, True)
  273. mqtt_client[flora_name.lower()].publish('{}/moisture/$retained'.format(sensor_path), 'true', 1, True)
  274. mqtt_client[flora_name.lower()].publish('{}/temperature/$name'.format(sensor_path), 'temperature', 1, True)
  275. mqtt_client[flora_name.lower()].publish('{}/temperature/$settable'.format(sensor_path), 'false', 1, True)
  276. mqtt_client[flora_name.lower()].publish('{}/temperature/$unit'.format(sensor_path), '°C', 1, True)
  277. mqtt_client[flora_name.lower()].publish('{}/temperature/$datatype'.format(sensor_path), 'float', 1, True)
  278. mqtt_client[flora_name.lower()].publish('{}/temperature/$format'.format(sensor_path), '*', 1, True)
  279. mqtt_client[flora_name.lower()].publish('{}/temperature/$retained'.format(sensor_path), 'true', 1, True)
  280. sleep(0.5) # some slack for the publish roundtrip and callback function
  281. print()
  282. elif reporting_mode == 'homeassistant-mqtt':
  283. print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...')
  284. for [flora_name, flora] in flores.items():
  285. state_topic = '{}/sensor/{}/state'.format(base_topic, flora_name.lower())
  286. for [sensor, params] in parameters.items():
  287. discovery_topic = 'homeassistant/sensor/{}/{}/config'.format(flora_name.lower(), sensor)
  288. payload = OrderedDict()
  289. payload['name'] = "{} {}".format(flora_name, sensor.title())
  290. payload['unique_id'] = "{}-{}".format(flora['mac'].lower().replace(":", ""), sensor)
  291. payload['unit_of_measurement'] = params['unit']
  292. if 'device_class' in params:
  293. payload['device_class'] = params['device_class']
  294. if 'state_class' in params:
  295. payload['state_class'] = params['state_class']
  296. payload['state_topic'] = state_topic
  297. payload['value_template'] = "{{{{ value_json.{} }}}}".format(sensor)
  298. payload['device'] = {
  299. 'identifiers' : ["MiFlora{}".format(flora['mac'].lower().replace(":", ""))],
  300. 'connections' : [["mac", flora['mac'].lower()]],
  301. 'manufacturer' : 'Xiaomi',
  302. 'name' : flora_name,
  303. 'model' : 'MiFlora Plant Sensor (HHCCJCY01)',
  304. 'sw_version': flora['firmware']
  305. }
  306. payload['expire_after'] = '3600'
  307. mqtt_client.publish(discovery_topic, json.dumps(payload), 1, True)
  308. elif reporting_mode == 'gladys-mqtt':
  309. print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...')
  310. for [flora_name, flora] in flores.items():
  311. topic_path = '{}/mqtt:miflora:{}/feature'.format(base_topic, flora_name.lower())
  312. data = OrderedDict()
  313. for param,_ in parameters.items():
  314. data[param] = flora['poller'].parameter_value(param)
  315. mqtt_client.publish('{}/mqtt:battery/state'.format(topic_path),data['battery'],1,True)
  316. mqtt_client.publish('{}/mqtt:moisture/state'.format(topic_path),data['moisture'],1,True)
  317. mqtt_client.publish('{}/mqtt:light/state'.format(topic_path),data['light'],1,True)
  318. mqtt_client.publish('{}/mqtt:conductivity/state'.format(topic_path),data['conductivity'],1,True)
  319. mqtt_client.publish('{}/mqtt:temperature/state'.format(topic_path),data['temperature'],1,True)
  320. sleep(0.5) # some slack for the publish roundtrip and callback function
  321. print()
  322. elif reporting_mode == 'wirenboard-mqtt':
  323. print_line('Announcing Mi Flora devices to MQTT broker for auto-discovery ...')
  324. for [flora_name, flora] in flores.items():
  325. mqtt_client.publish('/devices/{}/meta/name'.format(flora_name), flora_name, 1, True)
  326. topic_path = '/devices/{}/controls'.format(flora_name)
  327. mqtt_client.publish('{}/battery/meta/type'.format(topic_path), 'value', 1, True)
  328. mqtt_client.publish('{}/battery/meta/units'.format(topic_path), '%', 1, True)
  329. mqtt_client.publish('{}/conductivity/meta/type'.format(topic_path), 'value', 1, True)
  330. mqtt_client.publish('{}/conductivity/meta/units'.format(topic_path), 'µS/cm', 1, True)
  331. mqtt_client.publish('{}/light/meta/type'.format(topic_path), 'value', 1, True)
  332. mqtt_client.publish('{}/light/meta/units'.format(topic_path), 'lux', 1, True)
  333. mqtt_client.publish('{}/moisture/meta/type'.format(topic_path), 'rel_humidity', 1, True)
  334. mqtt_client.publish('{}/temperature/meta/type'.format(topic_path), 'temperature', 1, True)
  335. mqtt_client.publish('{}/timestamp/meta/type'.format(topic_path), 'text', 1, True)
  336. sleep(0.5) # some slack for the publish roundtrip and callback function
  337. print()
  338. print_line('Initialization complete, starting MQTT publish loop', console=False, sd_notify=True)
  339. # Sensor data retrieval and publication
  340. while True:
  341. for [flora_name, flora] in flores.items():
  342. data = OrderedDict()
  343. attempts = 2
  344. flora['poller']._cache = None
  345. flora['poller']._last_read = None
  346. flora['stats']['count'] += 1
  347. print_line('Retrieving data from sensor "{}" ...'.format(flora['name_pretty']))
  348. while attempts != 0 and not flora['poller']._cache:
  349. try:
  350. flora['poller'].fill_cache()
  351. flora['poller'].parameter_value(MI_LIGHT)
  352. except (IOError, BluetoothBackendException, BTLEException, RuntimeError, BrokenPipeError) as e:
  353. attempts -= 1
  354. if attempts > 0:
  355. if len(str(e)) > 0:
  356. print_line('Retrying due to exception: {}'.format(e), error=True)
  357. else:
  358. print_line('Retrying ...', warning=True)
  359. flora['poller']._cache = None
  360. flora['poller']._last_read = None
  361. if not flora['poller']._cache:
  362. flora['stats']['failure'] += 1
  363. if reporting_mode == 'mqtt-homie':
  364. mqtt_client[flora_name.lower()].publish('{}/{}/$state'.format(base_topic, flora_name.lower()), 'disconnected', 1, True)
  365. print_line('Failed to retrieve data from Mi Flora sensor "{}" ({}), success rate: {:.0%}'.format(
  366. flora['name_pretty'], flora['mac'], flora['stats']['success']/flora['stats']['count']
  367. ), error = True, sd_notify = True)
  368. print()
  369. continue
  370. else:
  371. flora['stats']['success'] += 1
  372. for param,_ in parameters.items():
  373. data[param] = flora['poller'].parameter_value(param)
  374. print_line('Result: {}'.format(json.dumps(data)))
  375. if reporting_mode == 'mqtt-json':
  376. print_line('Publishing to MQTT topic "{}/{}"'.format(base_topic, flora_name))
  377. mqtt_client.publish('{}/{}'.format(base_topic, flora_name), json.dumps(data))
  378. sleep(0.5) # some slack for the publish roundtrip and callback function
  379. elif reporting_mode == 'thingsboard-json':
  380. print_line('Publishing to MQTT topic "{}" username "{}"'.format(base_topic, flora_name))
  381. mqtt_client.username_pw_set(flora_name)
  382. mqtt_client.reconnect()
  383. sleep(1.0)
  384. mqtt_client.publish('{}'.format(base_topic), json.dumps(data))
  385. sleep(0.5) # some slack for the publish roundtrip and callback function
  386. elif reporting_mode == 'homeassistant-mqtt':
  387. print_line('Publishing to MQTT topic "{}/sensor/{}/state"'.format(base_topic, flora_name.lower()))
  388. mqtt_client.publish('{}/sensor/{}/state'.format(base_topic, flora_name.lower()), json.dumps(data))
  389. sleep(0.5) # some slack for the publish roundtrip and callback function
  390. elif reporting_mode == 'gladys-mqtt':
  391. print_line('Publishing to MQTT topic "{}/mqtt:miflora:{}/feature"'.format(base_topic, flora_name.lower()))
  392. mqtt_client.publish('{}/mqtt:miflora:{}/feature'.format(base_topic, flora_name.lower()), json.dumps(data))
  393. sleep(0.5) # some slack for the publish roundtrip and callback function
  394. elif reporting_mode == 'mqtt-homie':
  395. print_line('Publishing data to MQTT base topic "{}/{}"'.format(base_topic, flora_name.lower()))
  396. mqtt_client[flora_name.lower()].publish('{}/{}/$state'.format(base_topic, flora_name.lower()), 'ready', 1, True)
  397. for [param, value] in data.items():
  398. mqtt_client[flora_name.lower()].publish('{}/{}/sensor/{}'.format(base_topic, flora_name.lower(), param), value, 1, True)
  399. mqtt_client[flora_name.lower()].publish('{}/{}/$stats/timestamp'.format(base_topic, flora_name.lower()), strftime('%Y-%m-%dT%H:%M:%S%z', localtime()), 1, True)
  400. sleep(0.5) # some slack for the publish roundtrip and callback function
  401. elif reporting_mode == 'mqtt-smarthome':
  402. for [param, value] in data.items():
  403. print_line('Publishing data to MQTT topic "{}/status/{}/{}"'.format(base_topic, flora_name, param))
  404. payload = dict()
  405. payload['val'] = value
  406. payload['ts'] = int(round(time() * 1000))
  407. mqtt_client.publish('{}/status/{}/{}'.format(base_topic, flora_name, param), json.dumps(payload), retain=True)
  408. sleep(0.5) # some slack for the publish roundtrip and callback function
  409. elif reporting_mode == 'wirenboard-mqtt':
  410. for [param, value] in data.items():
  411. print_line('Publishing data to MQTT topic "/devices/{}/controls/{}"'.format(flora_name, param))
  412. mqtt_client.publish('/devices/{}/controls/{}'.format(flora_name, param), value, retain=True)
  413. mqtt_client.publish('/devices/{}/controls/{}'.format(flora_name, 'timestamp'), strftime('%Y-%m-%d %H:%M:%S', localtime()), retain=True)
  414. sleep(0.5) # some slack for the publish roundtrip and callback function
  415. elif reporting_mode == 'json':
  416. data['timestamp'] = strftime('%Y-%m-%d %H:%M:%S', localtime())
  417. data['name'] = flora_name
  418. data['name_pretty'] = flora['name_pretty']
  419. data['mac'] = flora['mac']
  420. data['firmware'] = flora['firmware']
  421. print('Data for "{}": {}'.format(flora_name, json.dumps(data)))
  422. else:
  423. raise NameError('Unexpected reporting_mode.')
  424. print()
  425. print_line('Status messages published', console=False, sd_notify=True)
  426. if daemon_enabled:
  427. print_line('Sleeping ({} seconds) ...'.format(sleep_period))
  428. sleep(sleep_period)
  429. print()
  430. else:
  431. print_line('Execution finished in non-daemon-mode', sd_notify=True)
  432. if reporting_mode == 'mqtt-json':
  433. mqtt_client.disconnect()
  434. break