Merge pull request #1520 from alexbelgium/mqtt_hook

mqtt_hook branch
This commit is contained in:
Alexandre
2024-08-14 12:10:26 +02:00
committed by GitHub
3 changed files with 64 additions and 108 deletions

View File

@@ -2,6 +2,19 @@
# shellcheck shell=bash # shellcheck shell=bash
set -e set -e
common_steps () {
# Copy script
cp /helpers/birdnet_to_mqtt.py "$HOME"/BirdNET-Pi/scripts/utils/birdnet_to_mqtt.py
chown pi:pi "$HOME"/BirdNET-Pi/scripts/utils/birdnet_to_mqtt.py
chmod +x "$HOME"/BirdNET-Pi/scripts/utils/birdnet_to_mqtt.py
# Add hooks
sed -i "/load_global_model, run_analysis/a from utils.birdnet_to_mqtt import automatic_mqtt_publish" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
sed -i "/write_to_db\(/a\ automatic_mqtt_publish(file, detections, os.path.basename(detection.file_name_extr))" "$HOME"/BirdNET-Pi/scripts/birdnet_analysis.py
}
if bashio::services.available 'mqtt' && ! bashio::config.true 'MQTT_DISABLED' ; then if bashio::services.available 'mqtt' && ! bashio::config.true 'MQTT_DISABLED' ; then
bashio::log.green "---" bashio::log.green "---"
bashio::log.blue "MQTT addon is active on your system! Birdnet-pi is now automatically configured to send its ouptut to MQTT" bashio::log.blue "MQTT addon is active on your system! Birdnet-pi is now automatically configured to send its ouptut to MQTT"
@@ -19,11 +32,9 @@ if bashio::services.available 'mqtt' && ! bashio::config.true 'MQTT_DISABLED' ;
sed -i "s|%%mqtt_user%%|$(bashio::services "mqtt" "username")|g" /helpers/birdnet_to_mqtt.py sed -i "s|%%mqtt_user%%|$(bashio::services "mqtt" "username")|g" /helpers/birdnet_to_mqtt.py
sed -i "s|%%mqtt_pass%%|$(bashio::services "mqtt" "password")|g" /helpers/birdnet_to_mqtt.py sed -i "s|%%mqtt_pass%%|$(bashio::services "mqtt" "password")|g" /helpers/birdnet_to_mqtt.py
# Copy script # Common steps
cp /helpers/birdnet_to_mqtt.py /usr/bin/birdnet_to_mqtt.py common_steps
cp /helpers/birdnet_to_mqtt.sh /custom-services.d
chmod 777 /usr/bin/birdnet_to_mqtt.py
chmod 777 /custom-services.d/birdnet_to_mqtt.sh
elif bashio::config.has_value "MQTT_HOST_manual" && bashio::config.has_value "MQTT_PORT_manual"; then elif bashio::config.has_value "MQTT_HOST_manual" && bashio::config.has_value "MQTT_PORT_manual"; then
bashio::log.green "---" bashio::log.green "---"
bashio::log.blue "MQTT is manually configured in the addon options" bashio::log.blue "MQTT is manually configured in the addon options"
@@ -39,9 +50,7 @@ elif bashio::config.has_value "MQTT_HOST_manual" && bashio::config.has_value "MQ
sed -i "s|%%mqtt_user%%|$(bashio::config "MQTT_USER_manual")|g" /helpers/birdnet_to_mqtt.py sed -i "s|%%mqtt_user%%|$(bashio::config "MQTT_USER_manual")|g" /helpers/birdnet_to_mqtt.py
sed -i "s|%%mqtt_pass%%|$(bashio::config "MQTT_PASSWORD_manual")|g" /helpers/birdnet_to_mqtt.py sed -i "s|%%mqtt_pass%%|$(bashio::config "MQTT_PASSWORD_manual")|g" /helpers/birdnet_to_mqtt.py
# Copy script # Common steps
cp /helpers/birdnet_to_mqtt.py /usr/bin/birdnet_to_mqtt.py common_steps
cp /helpers/birdnet_to_mqtt.sh /custom-services.d
chmod +x /usr/bin/birdnet_to_mqtt.py
chmod +x /custom-services.d/birdnet_to_mqtt.sh
fi fi

View File

@@ -1,133 +1,70 @@
#! /usr/bin/env python3 #! /usr/bin/env python3
# birdnet_to_mqtt.py # birdnet_to_mqtt.py
#
# Adapted from : https://gist.github.com/deepcoder/c309087c456fc733435b47d83f4113ff
# Adapted from : https://gist.github.com/JuanMeeske/08b839246a62ff38778f701fc1da5554
#
# monitor the records in the syslog file for info from the birdnet system on birds that it detects
# publish this data to mqtt
#
import time import time
import re import re
import dateparser
import datetime import datetime
import json import json
import logging import logging
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
import subprocess
import requests import requests
import sys import sys
import os
sys.path.append('/home/pi/BirdNET-Pi/scripts/utils') sys.path.append('/home/pi/BirdNET-Pi/scripts/utils')
from helpers import get_settings from helpers import get_settings
# Setup basic configuration for logging
logging.basicConfig(level=logging.INFO)
log = logging.getLogger(__name__)
# Used in flickrimage # Used in flickrimage
flickr_images = {} flickr_images = {}
conf = get_settings() conf = get_settings()
settings_dict = dict(conf) settings_dict = dict(conf)
# Setup basic configuration for logging # MQTT server configuration
logging.basicConfig(level=logging.INFO) mqtt_server = "%%mqtt_server%%"
mqtt_user = "%%mqtt_user%%"
mqtt_pass = "%%mqtt_pass%%"
mqtt_port = "%%mqtt_port%%"
mqtt_topic = 'birdnet'
# this generator function monitors the requested file handle for new lines added at its end
# the newly added line is returned by the function
def file_row_generator(s):
while True :
line = s.readline()
if not line:
time.sleep(0.1)
continue
yield line
# mqtt server
mqtt_server = "%%mqtt_server%%" # server for mqtt
mqtt_user = "%%mqtt_user%%" # Replace with your MQTT username
mqtt_pass = "%%mqtt_pass%%" # Replace with your MQTT password
mqtt_port = %%mqtt_port%% # port for mqtt
# mqtt topic for bird heard above threshold will be published
mqtt_topic_confident_birds = 'birdnet'
# url base for website that will be used to look up info about bird
bird_lookup_url_base = 'http://en.wikipedia.org/wiki/' bird_lookup_url_base = 'http://en.wikipedia.org/wiki/'
# regular expression patters used to decode the records from birdnet
re_high_clean = re.compile(r'(?<=^\[birdnet_analysis\]\[INFO\] ).*?(?=\.mp3$)')
syslog = open('/proc/1/fd/1', 'r')
def on_connect(client, userdata, flags, rc, properties=None): def on_connect(client, userdata, flags, rc, properties=None):
""" Callback for when the client receives a CONNACK response from the server. """ """ Callback for when the client receives a CONNACK response from the server. """
if rc == 0: if rc == 0:
logging.info("Connected to MQTT Broker!") log.info("Connected to MQTT Broker!")
else: else:
logging.error(f"Failed to connect, return code {rc}\n") log.error(f"Failed to connect, return code {rc}\n")
def get_bird_code(scientific_name): def get_bird_code(scientific_name):
with open('/home/pi/BirdNET-Pi/scripts/ebird.php', 'r') as file: with open('/home/pi/BirdNET-Pi/scripts/ebird.php', 'r') as file:
data = file.read() data = file.read()
# Extract the array from the PHP file
array_str = re.search(r'\$ebirds = \[(.*?)\];', data, re.DOTALL).group(1) array_str = re.search(r'\$ebirds = \[(.*?)\];', data, re.DOTALL).group(1)
# Convert the PHP array to a Python dictionary
bird_dict = {re.search(r'"(.*?)"', line).group(1): re.search(r'=> "(.*?)"', line).group(1) bird_dict = {re.search(r'"(.*?)"', line).group(1): re.search(r'=> "(.*?)"', line).group(1)
for line in array_str.split('\n') if '=>' in line} for line in array_str.split('\n') if '=>' in line}
# Return the corresponding value for the given bird's scientific name
return bird_dict.get(scientific_name) return bird_dict.get(scientific_name)
# this little hack is to make each received record for the all birds section unique def automatic_mqtt_publish(file, detections, path):
# the date and time that the log returns is only down to the 1 second accuracy, do for detection in detections:
# you can get multiple records with same date and time, this will make Home Assistant not
# think there is a new reading so we add a incrementing tenth of second to each record received
ts_noise = 0.0
#try :
# connect to MQTT server
mqttc = mqtt.Client('birdnet_mqtt') # Create instance of client with client ID
mqttc.username_pw_set(mqtt_user, mqtt_pass) # Use credentials
mqttc.connect(mqtt_server, mqtt_port) # Connect to (broker, port, keepalive-time)
mqttc.on_connect = on_connect
mqttc.loop_start()
# call the generator function and process each line that is returned
for row in file_row_generator(syslog):
# bird found above confidence level found, process it
if re_high_clean.search(row) :
# this slacker regular expression work, extracts the data about the bird found from the log line
# I do the parse in two passes, because I did not know the re to do it in one!
raw_high_bird = re.search(re_high_clean, row)
raw_high_bird = raw_high_bird.group(0)
# the fields we want are separated by semicolons, so split
high_bird_fields = raw_high_bird.split(';')
# build a structure in python that will be converted to json
bird = {} bird = {}
bird['Date'] = file.date
# human time in this record is in two fields, date and time. They are human format bird['Time'] = file.time
# combine them together separated by a space and they turn the human data into a python bird['ScientificName'] = detection.scientific_name.replace('_', ' ')
# timestamp bird['CommonName'] = detection.common_name
raw_ts = high_bird_fields[0] + ' ' + high_bird_fields[1] bird['Confidence'] = detection.confidence
bird['SpeciesCode'] = get_bird_code(detection.scientific_name)
#bird['ts'] = str(datetime.datetime.timestamp(dateparser.parse(raw_ts))) bird['ClipName'] = path
bird['Date'] = high_bird_fields[0] bird['url'] = bird_lookup_url_base + detection.scientific_name
bird['Time'] = high_bird_fields[1]
bird['ScientificName'] = high_bird_fields[2]
bird['CommonName'] = high_bird_fields[3]
bird['Confidence'] = high_bird_fields[4]
bird['SpeciesCode'] = get_bird_code(high_bird_fields[2])
bird['ClipName'] = high_bird_fields[11]
# build a url from scientific name of bird that can be used to lookup info about bird
bird['url'] = bird_lookup_url_base + high_bird_fields[2].replace(' ', '_')
# Flickimage # Flickimage
image_url = "" image_url = ""
common_name = high_bird_fields[3] common_name = detection.common_name
if len(settings_dict.get('FLICKR_API_KEY')) > 0: if len(settings_dict.get('FLICKR_API_KEY')) > 0:
if common_name not in flickr_images: if common_name not in flickr_images:
try: try:
@@ -140,17 +77,32 @@ for row in file_row_generator(syslog):
data = resp.json()["photos"]["photo"][0] data = resp.json()["photos"]["photo"][0]
image_url = 'https://farm'+str(data["farm"])+'.static.flickr.com/'+str(data["server"])+'/'+str(data["id"])+'_'+str(data["secret"])+'_n.jpg' image_url = 'https://farm'+str(data["farm"])+'.static.flickr.com/'+str(data["server"])+'/'+str(data["id"])+'_'+str(data["secret"])+'_n.jpg'
flickr_images[comName] = image_url flickr_images[common_name] = image_url
except Exception as e: except Exception as e:
print("FLICKR API ERROR: "+str(e)) print("FLICKR API ERROR: "+str(e))
image_url = "" image_url = ""
else: else:
image_url = flickr_images[comName] image_url = flickr_images[common_name]
bird['Flickrimage'] = image_url bird['Flickrimage'] = image_url
# convert to json string we can sent to mqtt
json_bird = json.dumps(bird) json_bird = json.dumps(bird)
mqttc.publish(mqtt_topic, json_bird, 1)
log.info("Posted to MQTT: ok")
print('Posted to MQTT : ok') mqttc = mqtt.Client('birdnet_mqtt')
mqttc.username_pw_set(mqtt_user, mqtt_pass)
mqttc.on_connect = on_connect
mqttc.publish(mqtt_topic_confident_birds, json_bird, 1) try:
mqttc.connect(mqtt_server, mqtt_port)
mqttc.loop_start()
# Assuming `file` and `detections` are provided from somewhere
# automatic_mqtt_publish(file, detections)
except Exception as e:
log.error("Cannot post mqtt: %s", e)
finally:
mqttc.loop_stop()
mqttc.disconnect()

View File

@@ -1,5 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
echo "Starting service: mqtt automated publish"
"$PYTHON_VIRTUAL_ENV" /usr/bin/birdnet_to_mqtt.py &>/proc/1/fd/1