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'),
'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'],
d['Time'], d['Week'], d['Lat'], d['Lon'], d['Cutoff'], d['Sens'], d['Overlap'], dict(conf))
notifications.sendAppriseNotifications(d['Sci_Name'], d['Com_Name'], d['Confidence'], round(d['Confidence'] * 100), d['File_Name'], d['Date'], d['Time'],
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 requests
import html
import time as timeim
import time
from .db import get_todays_count_for, get_this_weeks_count_for
from .helpers import get_settings
userDir = os.path.expanduser('~')
APPRISE_CONFIG = userDir + '/BirdNET-Pi/apprise.txt'
APPRISE_BODY = userDir + '/BirdNET-Pi/body.txt'
DB_PATH = userDir + '/BirdNET-Pi/scripts/birds.db'
apobj = None
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=""):
ret = template.replace("$sciname", sci_name) \
.replace("$comname", com_name) \
@@ -53,7 +53,7 @@ def sendAppriseNotifications(sci_name, com_name, confidence, confidencepct, path
.replace("$listenurl", listenurl) \
.replace("$friendlyurl", friendlyurl) \
.replace("$date", str(date)) \
.replace("$time", str(time)) \
.replace("$time", str(time_of_day)) \
.replace("$week", str(week)) \
.replace("$latitude", str(latitude)) \
.replace("$longitude", str(longitude)) \
@@ -64,41 +64,18 @@ def sendAppriseNotifications(sci_name, com_name, confidence, confidencepct, path
.replace("$overlap", str(overlap)) \
.replace("$reason", reason)
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'))
f = open(APPRISE_BODY, 'r')
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')
if len(websiteurl) == 0:
raise ValueError('Blank URL')
except Exception:
websiteurl = "http://"+socket.gethostname()+".local"
if websiteurl is None or len(websiteurl) == 0:
websiteurl = f"http://{socket.gethostname()}.local"
listenurl = websiteurl+"?filename="+path
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()
images[com_name] = resp['data']['image_url']
except Exception as e:
print("IMAGE API ERROR: "+str(e))
print("IMAGE API ERROR: " + str(e))
image_url = images.get(com_name, "")
if settings_dict.get('APPRISE_NOTIFY_EACH_DETECTION') == "1":
notify_body = render_template(body, "detection")
notify_title = render_template(title, "detection")
reason = "detection"
notify_body = render_template(body, reason)
notify_title = render_template(title, reason)
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.
if settings_dict.get('APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY') == "1":
numberDetections = get_todays_count_for(sci_name)
if 0 < numberDetections <= APPRISE_NOTIFICATION_NEW_SPECIES_DAILY_COUNT_LIMIT:
print("send the notification")
notify_body = render_template(body, "first time today")
notify_title = render_template(title, "first time today")
reason = "first time today"
notify_body = render_template(body, reason)
notify_title = render_template(title, reason)
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":
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_title = render_template(title, reason)
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__":
+1 -2
View File
@@ -159,8 +159,7 @@ def apprise(file: ParseFileName, detections: [Detection]):
try:
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),
conf['LATITUDE'], conf['LONGITUDE'], conf['CONFIDENCE'], conf['SENSITIVITY'],
conf['OVERLAP'], dict(conf))
conf['LATITUDE'], conf['LONGITUDE'], conf['CONFIDENCE'], conf['SENSITIVITY'], conf['OVERLAP'])
except BaseException as e:
log.exception('Error during Apprise:', exc_info=e)
+18 -11
View File
@@ -13,16 +13,23 @@ class Settings(dict):
@classmethod
def with_defaults(cls):
settings = {
'OVERLAP': 0.0,
'LATITUDE': 50,
'LONGITUDE': 5,
'CONFIDENCE': 0.7,
'DATABASE_LANG': 'en',
'PRIVACY_THRESHOLD': 0,
'EXTRACTION_LENGTH': 6,
'MODEL': 'BirdNET_GLOBAL_6K_V2.4_Model_FP16',
'DATA_MODEL_VERSION': 1,
'SENSITIVITY': 1.25,
'SF_THRESH': 0.003,
"OVERLAP": 0.0,
"LATITUDE": 50,
"LONGITUDE": 5,
"CONFIDENCE": 0.7,
"DATABASE_LANG": "en",
"PRIVACY_THRESHOLD": 0,
"EXTRACTION_LENGTH": 6,
"MODEL": "BirdNET_GLOBAL_6K_V2.4_Model_FP16",
"DATA_MODEL_VERSION": 1,
"SENSITIVITY": 1.25,
"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)
+82 -69
View File
@@ -1,15 +1,20 @@
import os
import sqlite3
from datetime import datetime
import unittest
from datetime import datetime
from unittest.mock import patch
from scripts.utils import db
from scripts.utils import notifications
from scripts.utils.notifications import sendAppriseNotifications
from tests.helpers import Settings
class TestAppriseNotifications(unittest.TestCase):
def setUp(self):
db.DB_PATH = self.db_file
@classmethod
def setUpClass(cls):
cls.db_file = "test.db"
@@ -59,32 +64,33 @@ class TestAppriseNotifications(unittest.TestCase):
if os.path.exists(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')
def test_notifications(self, mock_notify):
def test_notifications(self, mock_notify, mock_load_settings):
self.create_test_db()
self.create_apprise_config()
settings_dict = {
"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"
}
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)
notifications.DB_PATH = self.db_file
settings_dict = Settings.with_defaults()
mock_load_settings.return_value = settings_dict
sendAppriseNotifications(**self.get_default_params())
# No active apprise notifications configured. Confirm no notifications.
self.assertEqual(mock_notify.call_count, 0)
@@ -92,21 +98,8 @@ class TestAppriseNotifications(unittest.TestCase):
# Add daily notification.
mock_notify.reset_mock()
settings_dict["APPRISE_NOTIFY_NEW_SPECIES_EACH_DAY"] = "1"
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)
mock_load_settings.return_value = settings_dict
sendAppriseNotifications(**self.get_default_params())
self.assertEqual(mock_notify.call_count, 1)
self.assertEqual(
@@ -117,21 +110,8 @@ class TestAppriseNotifications(unittest.TestCase):
# Add new species notification.
mock_notify.reset_mock()
settings_dict["APPRISE_NOTIFY_NEW_SPECIES"] = "1"
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)
mock_load_settings.return_value = settings_dict
sendAppriseNotifications(**self.get_default_params())
self.assertEqual(mock_notify.call_count, 2)
self.assertEqual(
@@ -146,24 +126,57 @@ class TestAppriseNotifications(unittest.TestCase):
# Add each species notification.
mock_notify.reset_mock()
settings_dict["APPRISE_NOTIFY_EACH_DETECTION"] = "1"
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)
mock_load_settings.return_value = settings_dict
sendAppriseNotifications(**self.get_default_params())
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__':
unittest.main()