cleanup: factor out passing in settings

This commit is contained in:
frederik
2025-11-15 16:38:45 +01:00
parent d154b5f6f1
commit 1e2edbfd6e
5 changed files with 180 additions and 153 deletions
+2 -3
View File
@@ -38,6 +38,5 @@ if __name__ == "__main__":
'Lat': conf.getfloat('LATITUDE'), 'Lon': conf.getfloat('LONGITUDE'), 'Cutoff': conf.getfloat('CONFIDENCE'), 'Lat': conf.getfloat('LATITUDE'), 'Lon': conf.getfloat('LONGITUDE'), 'Cutoff': conf.getfloat('CONFIDENCE'),
'Sens': conf.getfloat('SENSITIVITY'), 'Overlap': conf.getfloat('OVERLAP')} 'Sens': conf.getfloat('SENSITIVITY'), 'Overlap': conf.getfloat('OVERLAP')}
notifications.sendAppriseNotifications(d['Sci_Name'], d['Com_Name'], d['Confidence'], round(d['Confidence'] * 100), d['File_Name'], d['Date'], notifications.sendAppriseNotifications(d['Sci_Name'], d['Com_Name'], d['Confidence'], round(d['Confidence'] * 100), d['File_Name'], d['Date'], d['Time'],
d['Time'], d['Week'], d['Lat'], d['Lon'], d['Cutoff'], d['Sens'], d['Overlap'], dict(conf)) d['Week'], d['Lat'], d['Lon'], d['Cutoff'], d['Sens'], d['Overlap'])
+51 -42
View File
@@ -3,14 +3,14 @@ import os
import socket import socket
import requests import requests
import html import html
import time as timeim import time
from .db import get_todays_count_for, get_this_weeks_count_for from .db import get_todays_count_for, get_this_weeks_count_for
from .helpers import get_settings
userDir = os.path.expanduser('~') userDir = os.path.expanduser('~')
APPRISE_CONFIG = userDir + '/BirdNET-Pi/apprise.txt' APPRISE_CONFIG = userDir + '/BirdNET-Pi/apprise.txt'
APPRISE_BODY = userDir + '/BirdNET-Pi/body.txt' APPRISE_BODY = userDir + '/BirdNET-Pi/body.txt'
DB_PATH = userDir + '/BirdNET-Pi/scripts/birds.db'
apobj = None apobj = None
images = {} images = {}
@@ -44,7 +44,7 @@ def notify(body, title, attached=""):
) )
def sendAppriseNotifications(sci_name, com_name, confidence, confidencepct, path, date, time, week, latitude, longitude, cutoff, sens, overlap, settings_dict): def sendAppriseNotifications(sci_name, com_name, confidence, confidencepct, path, date, time_of_day, week, latitude, longitude, cutoff, sens, overlap):
def render_template(template, reason=""): def render_template(template, reason=""):
ret = template.replace("$sciname", sci_name) \ ret = template.replace("$sciname", sci_name) \
.replace("$comname", com_name) \ .replace("$comname", com_name) \
@@ -53,7 +53,7 @@ def sendAppriseNotifications(sci_name, com_name, confidence, confidencepct, path
.replace("$listenurl", listenurl) \ .replace("$listenurl", listenurl) \
.replace("$friendlyurl", friendlyurl) \ .replace("$friendlyurl", friendlyurl) \
.replace("$date", str(date)) \ .replace("$date", str(date)) \
.replace("$time", str(time)) \ .replace("$time", str(time_of_day)) \
.replace("$week", str(week)) \ .replace("$week", str(week)) \
.replace("$latitude", str(latitude)) \ .replace("$latitude", str(latitude)) \
.replace("$longitude", str(longitude)) \ .replace("$longitude", str(longitude)) \
@@ -64,41 +64,18 @@ def sendAppriseNotifications(sci_name, com_name, confidence, confidencepct, path
.replace("$overlap", str(overlap)) \ .replace("$overlap", str(overlap)) \
.replace("$reason", reason) .replace("$reason", reason)
return ret return ret
# print(sendAppriseNotifications)
# print(settings_dict)
if os.path.exists(APPRISE_CONFIG) and os.path.getsize(APPRISE_CONFIG) > 0:
if not should_notify(com_name):
return
settings_dict = get_settings()
title = html.unescape(settings_dict.get('APPRISE_NOTIFICATION_TITLE')) title = html.unescape(settings_dict.get('APPRISE_NOTIFICATION_TITLE'))
f = open(APPRISE_BODY, 'r') f = open(APPRISE_BODY, 'r')
body = f.read() body = f.read()
APPRISE_ONLY_NOTIFY_SPECIES_NAMES = settings_dict.get('APPRISE_ONLY_NOTIFY_SPECIES_NAMES')
if APPRISE_ONLY_NOTIFY_SPECIES_NAMES is not None and APPRISE_ONLY_NOTIFY_SPECIES_NAMES.strip() != "":
if any(bird.lower().replace(" ", "") in com_name.lower().replace(" ", "") for bird in APPRISE_ONLY_NOTIFY_SPECIES_NAMES.split(",")):
return
APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 = settings_dict.get('APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2')
if APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 is not None and APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2.strip() != "":
if not any(bird.lower().replace(" ", "") in com_name.lower().replace(" ", "") for bird in APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2.split(",")):
return
APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES = settings_dict.get('APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES')
if APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES != "0":
if species_last_notified.get(com_name) is not None:
try:
if int(timeim.time()) - species_last_notified[com_name] < int(APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES):
return
except Exception as e:
print("APPRISE NOTIFICATION EXCEPTION: "+str(e))
return
# TODO: this all needs to be changed, we changed the caddy default to allow direct IP access, so birdnetpi.local shouldn't be relied on anymore
try:
websiteurl = settings_dict.get('BIRDNETPI_URL') websiteurl = settings_dict.get('BIRDNETPI_URL')
if len(websiteurl) == 0: if websiteurl is None or len(websiteurl) == 0:
raise ValueError('Blank URL') websiteurl = f"http://{socket.gethostname()}.local"
except Exception:
websiteurl = "http://"+socket.gethostname()+".local"
listenurl = websiteurl+"?filename="+path listenurl = websiteurl+"?filename="+path
friendlyurl = "[Listen here]("+listenurl+")" friendlyurl = "[Listen here]("+listenurl+")"
@@ -111,24 +88,25 @@ def sendAppriseNotifications(sci_name, com_name, confidence, confidencepct, path
resp = requests.get(url=url, timeout=10).json() resp = requests.get(url=url, timeout=10).json()
images[com_name] = resp['data']['image_url'] images[com_name] = resp['data']['image_url']
except Exception as e: except Exception as e:
print("IMAGE API ERROR: "+str(e)) print("IMAGE API ERROR: " + str(e))
image_url = images.get(com_name, "") image_url = images.get(com_name, "")
if settings_dict.get('APPRISE_NOTIFY_EACH_DETECTION') == "1": if settings_dict.get('APPRISE_NOTIFY_EACH_DETECTION') == "1":
notify_body = render_template(body, "detection") reason = "detection"
notify_title = render_template(title, "detection") notify_body = render_template(body, reason)
notify_title = render_template(title, reason)
notify(notify_body, notify_title, image_url) notify(notify_body, notify_title, image_url)
species_last_notified[com_name] = int(timeim.time()) species_last_notified[com_name] = int(time.time())
APPRISE_NOTIFICATION_NEW_SPECIES_DAILY_COUNT_LIMIT = 1 # Notifies the first N per day. APPRISE_NOTIFICATION_NEW_SPECIES_DAILY_COUNT_LIMIT = 1 # Notifies the first N per day.
if settings_dict.get('APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY') == "1": if settings_dict.get('APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY') == "1":
numberDetections = get_todays_count_for(sci_name) numberDetections = get_todays_count_for(sci_name)
if 0 < numberDetections <= APPRISE_NOTIFICATION_NEW_SPECIES_DAILY_COUNT_LIMIT: if 0 < numberDetections <= APPRISE_NOTIFICATION_NEW_SPECIES_DAILY_COUNT_LIMIT:
print("send the notification") reason = "first time today"
notify_body = render_template(body, "first time today") notify_body = render_template(body, reason)
notify_title = render_template(title, "first time today") notify_title = render_template(title, reason)
notify(notify_body, notify_title, image_url) notify(notify_body, notify_title, image_url)
species_last_notified[com_name] = int(timeim.time()) species_last_notified[com_name] = int(time.time())
if settings_dict.get('APPRISE_NOTIFY_NEW_SPECIES') == "1": if settings_dict.get('APPRISE_NOTIFY_NEW_SPECIES') == "1":
numberDetections = get_this_weeks_count_for(sci_name) numberDetections = get_this_weeks_count_for(sci_name)
@@ -137,7 +115,38 @@ def sendAppriseNotifications(sci_name, com_name, confidence, confidencepct, path
notify_body = render_template(body, reason) notify_body = render_template(body, reason)
notify_title = render_template(title, reason) notify_title = render_template(title, reason)
notify(notify_body, notify_title, image_url) notify(notify_body, notify_title, image_url)
species_last_notified[com_name] = int(timeim.time()) species_last_notified[com_name] = int(time.time())
def should_notify(com_name):
settings_dict = get_settings()
if not (os.path.exists(APPRISE_CONFIG) and os.path.getsize(APPRISE_CONFIG) > 0):
return False
# check if this is an excluded species
APPRISE_ONLY_NOTIFY_SPECIES_NAMES = settings_dict.get('APPRISE_ONLY_NOTIFY_SPECIES_NAMES')
if APPRISE_ONLY_NOTIFY_SPECIES_NAMES is not None and APPRISE_ONLY_NOTIFY_SPECIES_NAMES.strip() != "":
if any(bird.lower().replace(" ", "") in com_name.lower().replace(" ", "") for bird in APPRISE_ONLY_NOTIFY_SPECIES_NAMES.split(",")):
return False
# check if this is an included species
APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 = settings_dict.get('APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2')
if APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2 is not None and APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2.strip() != "":
if not any(bird.lower().replace(" ", "") in com_name.lower().replace(" ", "") for bird in APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2.split(",")):
return False
# is it still too soon?
APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES = settings_dict.get('APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES')
if APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES != "0":
if species_last_notified.get(com_name) is not None:
try:
if int(time.time()) - species_last_notified[com_name] < int(APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES):
return False
except Exception as e:
print("APPRISE NOTIFICATION EXCEPTION: " + str(e))
return False
return True
if __name__ == "__main__": if __name__ == "__main__":
+1 -2
View File
@@ -159,8 +159,7 @@ def apprise(file: ParseFileName, detections: [Detection]):
try: try:
sendAppriseNotifications(detection.scientific_name, detection.common_name, str(detection.confidence), str(detection.confidence_pct), sendAppriseNotifications(detection.scientific_name, detection.common_name, str(detection.confidence), str(detection.confidence_pct),
os.path.basename(detection.file_name_extr), detection.date, detection.time, str(detection.week), os.path.basename(detection.file_name_extr), detection.date, detection.time, str(detection.week),
conf['LATITUDE'], conf['LONGITUDE'], conf['CONFIDENCE'], conf['SENSITIVITY'], conf['LATITUDE'], conf['LONGITUDE'], conf['CONFIDENCE'], conf['SENSITIVITY'], conf['OVERLAP'])
conf['OVERLAP'], dict(conf))
except BaseException as e: except BaseException as e:
log.exception('Error during Apprise:', exc_info=e) log.exception('Error during Apprise:', exc_info=e)
+18 -11
View File
@@ -13,16 +13,23 @@ class Settings(dict):
@classmethod @classmethod
def with_defaults(cls): def with_defaults(cls):
settings = { settings = {
'OVERLAP': 0.0, "OVERLAP": 0.0,
'LATITUDE': 50, "LATITUDE": 50,
'LONGITUDE': 5, "LONGITUDE": 5,
'CONFIDENCE': 0.7, "CONFIDENCE": 0.7,
'DATABASE_LANG': 'en', "DATABASE_LANG": "en",
'PRIVACY_THRESHOLD': 0, "PRIVACY_THRESHOLD": 0,
'EXTRACTION_LENGTH': 6, "EXTRACTION_LENGTH": 6,
'MODEL': 'BirdNET_GLOBAL_6K_V2.4_Model_FP16', "MODEL": "BirdNET_GLOBAL_6K_V2.4_Model_FP16",
'DATA_MODEL_VERSION': 1, "DATA_MODEL_VERSION": 1,
'SENSITIVITY': 1.25, "SENSITIVITY": 1.25,
'SF_THRESH': 0.003, "SF_THRESH": 0.003,
"APPRISE_NOTIFICATION_TITLE": "New backyard bird!",
"APPRISE_NOTIFY_EACH_DETECTION": "0",
"APPRISE_NOTIFY_NEW_SPECIES": "0",
"APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY": "0",
"APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES": "0",
"APPRISE_ONLY_NOTIFY_SPECIES_NAMES": "",
"APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2": ""
} }
return cls(settings) return cls(settings)
+82 -69
View File
@@ -1,15 +1,20 @@
import os import os
import sqlite3 import sqlite3
from datetime import datetime
import unittest import unittest
from datetime import datetime
from unittest.mock import patch from unittest.mock import patch
from scripts.utils import db
from scripts.utils import notifications from scripts.utils import notifications
from scripts.utils.notifications import sendAppriseNotifications from scripts.utils.notifications import sendAppriseNotifications
from tests.helpers import Settings
class TestAppriseNotifications(unittest.TestCase): class TestAppriseNotifications(unittest.TestCase):
def setUp(self):
db.DB_PATH = self.db_file
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
cls.db_file = "test.db" cls.db_file = "test.db"
@@ -59,32 +64,33 @@ class TestAppriseNotifications(unittest.TestCase):
if os.path.exists(self.apprise_config_file): if os.path.exists(self.apprise_config_file):
os.remove(self.apprise_config_file) os.remove(self.apprise_config_file)
def get_default_params(self):
return {
"sci_name": "Myiarchus crinitus",
"com_name": "Great Crested Flycatcher",
"confidence": "0.91",
"confidencepct": "91",
"path": "filename",
"date": "1666-06-06",
"time_of_day": "06:06:06",
"week": "06",
"latitude": "-1",
"longitude": "-1",
"cutoff": "0.7",
"sens": "1.25",
"overlap": "0.0"
}
@patch('scripts.utils.helpers._load_settings')
@patch('scripts.utils.notifications.notify') @patch('scripts.utils.notifications.notify')
def test_notifications(self, mock_notify): def test_notifications(self, mock_notify, mock_load_settings):
self.create_test_db() self.create_test_db()
self.create_apprise_config() self.create_apprise_config()
settings_dict = { notifications.DB_PATH = self.db_file
"APPRISE_NOTIFICATION_TITLE": "New backyard bird!", settings_dict = Settings.with_defaults()
"APPRISE_NOTIFY_EACH_DETECTION": "0",
"APPRISE_NOTIFY_NEW_SPECIES": "0", mock_load_settings.return_value = settings_dict
"APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY": "0", sendAppriseNotifications(**self.get_default_params())
"APPRISE_MINIMUM_SECONDS_BETWEEN_NOTIFICATIONS_PER_SPECIES": "0"
}
sendAppriseNotifications("Myiarchus crinitus",
"Great Crested Flycatcher",
"0.91",
"91",
"filename",
"1666-06-06",
"06:06:06",
"06",
"-1",
"-1",
"0.7",
"1.25",
"0.0",
settings_dict,
self.db_file)
# No active apprise notifications configured. Confirm no notifications. # No active apprise notifications configured. Confirm no notifications.
self.assertEqual(mock_notify.call_count, 0) self.assertEqual(mock_notify.call_count, 0)
@@ -92,21 +98,8 @@ class TestAppriseNotifications(unittest.TestCase):
# Add daily notification. # Add daily notification.
mock_notify.reset_mock() mock_notify.reset_mock()
settings_dict["APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY"] = "1" settings_dict["APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY"] = "1"
sendAppriseNotifications("Myiarchus crinitus", mock_load_settings.return_value = settings_dict
"Great Crested Flycatcher", sendAppriseNotifications(**self.get_default_params())
"0.91",
"91",
"filename",
"1666-06-06",
"06:06:06",
"06",
"-1",
"-1",
"0.7",
"1.25",
"0.0",
settings_dict,
self.db_file)
self.assertEqual(mock_notify.call_count, 1) self.assertEqual(mock_notify.call_count, 1)
self.assertEqual( self.assertEqual(
@@ -117,21 +110,8 @@ class TestAppriseNotifications(unittest.TestCase):
# Add new species notification. # Add new species notification.
mock_notify.reset_mock() mock_notify.reset_mock()
settings_dict["APPRISE_NOTIFY_NEW_SPECIES"] = "1" settings_dict["APPRISE_NOTIFY_NEW_SPECIES"] = "1"
sendAppriseNotifications("Myiarchus crinitus", mock_load_settings.return_value = settings_dict
"Great Crested Flycatcher", sendAppriseNotifications(**self.get_default_params())
"0.91",
"91",
"filename",
"1666-06-06",
"06:06:06",
"06",
"-1",
"-1",
"0.7",
"1.25",
"0.0",
settings_dict,
self.db_file)
self.assertEqual(mock_notify.call_count, 2) self.assertEqual(mock_notify.call_count, 2)
self.assertEqual( self.assertEqual(
@@ -146,24 +126,57 @@ class TestAppriseNotifications(unittest.TestCase):
# Add each species notification. # Add each species notification.
mock_notify.reset_mock() mock_notify.reset_mock()
settings_dict["APPRISE_NOTIFY_EACH_DETECTION"] = "1" settings_dict["APPRISE_NOTIFY_EACH_DETECTION"] = "1"
sendAppriseNotifications("Myiarchus crinitus", mock_load_settings.return_value = settings_dict
"Great Crested Flycatcher", sendAppriseNotifications(**self.get_default_params())
"0.91",
"91",
"filename",
"1666-06-06",
"06:06:06",
"06",
"-1",
"-1",
"0.7",
"1.25",
"0.0",
settings_dict,
self.db_file)
self.assertEqual(mock_notify.call_count, 3) self.assertEqual(mock_notify.call_count, 3)
@patch('scripts.utils.helpers._load_settings')
@patch('scripts.utils.notifications.notify')
def test_notifications_excluded(self, mock_notify, mock_load_settings):
self.create_test_db()
self.create_apprise_config()
notifications.DB_PATH = self.db_file
settings_dict = Settings.with_defaults()
settings_dict["APPRISE_NOTIFY_EACH_DETECTION"] = "1"
settings_dict['APPRISE_ONLY_NOTIFY_SPECIES_NAMES'] = 'Quailfinch'
mock_load_settings.return_value = settings_dict
sendAppriseNotifications(**self.get_default_params())
# Not excluded. Confirm notifications.
self.assertEqual(mock_notify.call_count, 1)
mock_notify.reset_mock()
settings_dict['APPRISE_ONLY_NOTIFY_SPECIES_NAMES'] = 'Quailfinch,Great Crested Flycatcher'
mock_load_settings.return_value = settings_dict
sendAppriseNotifications(**self.get_default_params())
self.assertEqual(mock_notify.call_count, 0)
@patch('scripts.utils.helpers._load_settings')
@patch('scripts.utils.notifications.notify')
def test_notifications_included(self, mock_notify, mock_load_settings):
self.create_test_db()
self.create_apprise_config()
notifications.DB_PATH = self.db_file
settings_dict = Settings.with_defaults()
settings_dict["APPRISE_NOTIFY_EACH_DETECTION"] = "1"
settings_dict['APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2'] = 'Quailfinch'
mock_load_settings.return_value = settings_dict
sendAppriseNotifications(**self.get_default_params())
# No wanted species. Confirm no notifications.
self.assertEqual(mock_notify.call_count, 0)
mock_notify.reset_mock()
settings_dict['APPRISE_ONLY_NOTIFY_SPECIES_NAMES_2'] = 'Quailfinch,Great Crested Flycatcher'
mock_load_settings.return_value = settings_dict
sendAppriseNotifications(**self.get_default_params())
self.assertEqual(mock_notify.call_count, 1)
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()