Просмотр исходного кода

Support homie convention, first draft

Thomas Dietrich 8 лет назад
Родитель
Сommit
37e90ec1c9
3 измененных файлов с 87 добавлено и 30 удалено
  1. 5 4
      README.md
  2. 10 4
      config.ini
  3. 72 22
      miflora-mqtt-daemon.py

+ 5 - 4
README.md

@@ -13,13 +13,14 @@ The program can be executed for a single run or in **daemon mode** to run contin
 * Build on top of [open-homeautomation/miflora](https://github.com/open-homeautomation/miflora)
 * Highly configurable
 * Data publication via MQTT
-* JSON encoded
+* JSON encoded or following the [Homie Convention](https://github.com/marvinroger/homie)
+* Announcement messages to support auto-discovery services
 * MQTT authentication support
 * Daemon mode (default)
 * Systemd service file included, sd\_notify messages generated
 * MQTT-less mode, printing data directly to stdout/file
 * Reliable and inituitive
-* Tested on Raspberry Pi
+* Tested on Raspberry Pi 3 and 0W
 
 ![Promotional image](https://xiaomi-mi.com/uploads/ck/xiaomi-flower-monitor-001.jpg)
 
@@ -31,7 +32,7 @@ The Mi Flora sensor offers the following plant and soil readings:
 |-----------------|-------------|
 | `temperature`   | Air temperature, in [°C] (0.1°C resolution) |
 | `light`         | [Sunlight intensity](https://aquarium-digest.com/tag/lumenslux-requirements-of-a-cannabis-plant/), in [lux] |
-| `moisture`      | (Soil moisture](https://observant.zendesk.com/hc/en-us/articles/208067926-Monitoring-Soil-Moisture-for-Optimal-Crop-Growth), in [%] |
+| `moisture`      | [Soil moisture](https://observant.zendesk.com/hc/en-us/articles/208067926-Monitoring-Soil-Moisture-for-Optimal-Crop-Growth), in [%] |
 | `conductivity`  | [Soil fertility](https://www.plantcaretools.com/measure-fertilization-with-ec-meters-for-plants-faq), in [µS/cm] |
 | `battery`       | Sensor battery level, in [%] |
 
@@ -112,7 +113,7 @@ This can be done either by using the internal daemon or cron.
 
 ### Integration
 
-Data will be published to the MQTT broker topic "`miflora/sensorname`" (names configurable).
+In the "mqtt-json" reporting mode, data will be published to the MQTT broker topic "`miflora/sensorname`" (names configurable).
 An example:
 
 ```json

+ 10 - 4
config.ini

@@ -7,14 +7,15 @@
 
 # The operation mode of the program. Determines wether retrieved sensor data is published via MQTT or stdout/file.
 # Currently supported:
-#    mqtt-json - Publish to an mqtt broker, json encoded (default)
+#    mqtt-json - Publish to an mqtt broker, json encoded (Default)
+#   mqtt-homie - Publish to an mqtt broker following the Homie MQTT convention (https://github.com/marvinroger/homie)
 #         json - Print to stdout as json encoded string
 #reporting_method = mqtt-json
 
 [Daemon]
 
 # Enable or Disable an endless execution loop (Default: true)
-#enabled = false
+#enabled = true
 
 # The period between two measurements in seconds (Default: 300)
 #period = 300
@@ -30,8 +31,13 @@
 # Maximum period in seconds between ping messages to the broker. (Default: 60)
 #keepalive = 60
 
-# The MQTT base topic to publish all Mi Flora sensor data topics under (Default: miflora)
-#topic_prefix = miflora
+# The MQTT base topic to publish all Mi Flora sensor data topics under.
+# Default depends on the configured reporting_mode (mqtt-json: miflora, mqtt-homie: homie)
+#base_topic = miflora
+#base_topic = homie
+
+# Homie specific: The device ID for this daemon instance (Default: miflora-mqtt-daemon)
+#homie_device_id = miflora-mqtt-daemon
 
 # The MQTT broker authentification credentials (Default: no authentication)
 #username = user

+ 72 - 22
miflora-mqtt-daemon.py

@@ -30,7 +30,8 @@ def on_connect(client, userdata, flags, rc):
         os._exit(1)
 
 def on_publish(client, userdata, mid):
-    print('Data successfully published.')
+    #print('Data successfully published.')
+    pass
 
 # Load configuration file
 config = ConfigParser(delimiters=('=', ))
@@ -39,13 +40,14 @@ config.read([os.path.join(sys.path[0], 'config.ini'), os.path.join(sys.path[0],
 
 reporting_mode = config['General'].get('reporting_method', 'mqtt-json')
 daemon_enabled = config['Daemon'].getboolean('enabled', True)
-topic_prefix = config['MQTT'].get('topic_prefix', 'miflora')
+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')
 sleep_period = config['Daemon'].getint('period', 300)
 #miflora_cache_timeout = config['MiFlora'].getint('cache_timeout', 600)
 miflora_cache_timeout = sleep_period - 1
 
 # Check configuration
-if not reporting_mode in ['mqtt-json', 'json']:
+if not reporting_mode in ['mqtt-json', 'mqtt-homie', 'json']:
     print('Error. Configuration parameter reporting_mode set to an invalid value.', file=sys.stderr)
     sd_notifier.notify('STATUS=Configuration parameter reporting_mode set to an invalid value')
     sys.exit(1)
@@ -57,12 +59,15 @@ if not config['Sensors']:
 sd_notifier.notify('STATUS=Configuration accepted')
 
 # MQTT connection
-if reporting_mode == 'mqtt-json':
+if reporting_mode in ['mqtt-json', 'mqtt-homie']:
     print('Connecting to MQTT broker ...')
     mqtt_client = mqtt.Client()
     mqtt_client.on_connect = on_connect
     mqtt_client.on_publish = on_publish
-    mqtt_client.will_set('{}/$announce'.format(topic_prefix), payload='{}', retain=True)
+    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)
     if config['MQTT'].get('username'):
         mqtt_client.username_pw_set(config['MQTT'].get('username'), config['MQTT'].get('password', None))
     try:
@@ -82,26 +87,26 @@ sd_notifier.notify('READY=1')
 # Initialize Mi Flora sensors
 flores = dict()
 for [name, mac] in config['Sensors'].items():
-    sd_notifier.notify('STATUS=Attempting initial connection to MiFlora sensor "{}" ({})'.format(name, mac))
-    print('Adding sensor to device list and testing connection...')
+    flora = dict()
+    print('Adding sensor to device list and testing connection ...')
     print('Name:         "{}"'.format(name))
+    sd_notifier.notify('STATUS=Attempting initial connection to MiFlora sensor "{}" ({})'.format(name, mac))
     flora_poller = MiFloraPoller(mac=mac, cache_timeout=miflora_cache_timeout, retries=9)
+    flora['poller'] = flora_poller
+    flora['mac'] = flora_poller._mac
     try:
         flora_poller.fill_cache()
         flora_poller.parameter_value(MI_LIGHT)
+        flora['firmware'] = flora_poller.firmware_version()
     except IOError:
         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))
-        sys.exit(1)
-    print('Device name:  "{}"'.format(flora_poller.name()))
-    print('MAC address:  {}'.format(flora_poller._mac))
-    print('Firmware:     {}'.format(flora_poller.firmware_version()))
-    print()
-
-    flora = dict()
-    flora['mac'] = flora_poller._mac
-    flora['poller'] = flora_poller
-    flora['firmware'] = flora_poller.firmware_version()
+        continue
+    else:
+        print('Device name:  "{}"'.format(flora_poller.name()))
+        print('MAC address:  {}'.format(flora_poller._mac))
+        print('Firmware:     {}'.format(flora_poller.firmware_version()))
+        print()
     flora['refresh'] = sleep_period
     flora['location'] = ''
     flores[name] = flora
@@ -112,9 +117,44 @@ if reporting_mode == 'mqtt-json':
     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']}
-        flora_info['topic'] = '{}/{}'.format(topic_prefix, flora_name)
+        flora_info['topic'] = '{}/{}'.format(base_topic, flora_name)
         flores_info[flora_name] = flora_info
-    mqtt_client.publish('{}/$announce'.format(topic_prefix), json.dumps(flores_info), retain=True)
+    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':
+    print('Announcing MiFlora devices to MQTT broker for auto-discovery ...')
+    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)
     sleep(0.5) # some slack for the publish roundtrip and callback function
     print()
 
@@ -124,20 +164,30 @@ sd_notifier.notify('STATUS=Initialization complete, starting MQTT publish loop')
 while True:
     for [flora_name, flora] in flores.items():
         data = dict()
-        while not flora['poller']._cache:
+        retries = 3
+        while retries > 0 and not flora['poller']._cache:
             try:
                 flora['poller'].fill_cache()
             except IOError:
                 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']))
+                retries = retries - 1
+        if not flora['poller']._cache:
+            continue
         for param in parameters:
             data[param] = flora['poller'].parameter_value(param)
 
         timestamp = strftime('%Y-%m-%d %H:%M:%S', localtime())
 
         if reporting_mode == 'mqtt-json':
-            print('[{}] Attempting to publishing to MQTT topic "{}/{}" ...\nData: {}'.format(timestamp, topic_prefix, flora_name, json.dumps(data)))
-            mqtt_client.publish('{}/{}'.format(topic_prefix, flora_name), json.dumps(data))
+            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)
             sleep(0.5) # some slack for the publish roundtrip and callback function
             print()
         elif reporting_mode == 'json':