diff --git a/config_templates/birdnetpi.yaml b/config_templates/birdnetpi.yaml index 5dcad717..0297d526 100644 --- a/config_templates/birdnetpi.yaml +++ b/config_templates/birdnetpi.yaml @@ -121,10 +121,6 @@ notification_rules: [] notify_quiet_hours_start: "" # e.g., "22:00" for 10 PM (empty = no quiet hours) notify_quiet_hours_end: "" # e.g., "06:00" for 6 AM -# Flickr Integration -flickr_api_key: "" -flickr_filter_email: "" - # Localization and Species Display language: en # Language code for UI and species name translation species_display_mode: full # Options: "full" (Common Name (Scientific Name)), "common_name", "scientific_name" @@ -134,12 +130,6 @@ timezone: UTC enable_gps: false gps_update_interval: 5.0 -# Hardware Monitoring -hardware_check_interval: 10.0 -enable_audio_device_check: true -enable_system_resource_check: true -enable_gps_check: false - # Analysis Configuration privacy_threshold: 10.0 diff --git a/src/birdnetpi/audio/analysis.py b/src/birdnetpi/audio/analysis.py index 8a315ebb..0571840f 100644 --- a/src/birdnetpi/audio/analysis.py +++ b/src/birdnetpi/audio/analysis.py @@ -195,11 +195,11 @@ async def process_audio_chunk(self, audio_data_bytes: bytes) -> None: # Remove processed samples from buffer (keep overlap for continuity) # Clamp overlap to reasonable range (0.5 to 1.5 seconds, never >= buffer size) - overlap_seconds = min(self.config.analysis_overlap, 1.5) + overlap_seconds = min(self.config.audio_overlap, 1.5) if overlap_seconds >= 3.0: logger.warning( - "Invalid analysis_overlap %.1f seconds (must be < 3.0), using 1.0 second", - self.config.analysis_overlap, + "Invalid audio_overlap %.1f seconds (must be < 3.0), using 1.0 second", + self.config.audio_overlap, ) overlap_seconds = 1.0 overlap_samples = int(overlap_seconds * self.config.sample_rate) @@ -323,7 +323,7 @@ async def _send_detection_event( "species_confidence_threshold": self.config.species_confidence_threshold, "week": current_week, "sensitivity_setting": self.config.sensitivity_setting, - "overlap": self.config.analysis_overlap, + "overlap": self.config.audio_overlap, } # Try to send detection event to API diff --git a/src/birdnetpi/config/models.py b/src/birdnetpi/config/models.py index bae3be3c..9ecf18b5 100644 --- a/src/birdnetpi/config/models.py +++ b/src/birdnetpi/config/models.py @@ -75,7 +75,7 @@ class BirdNETConfig(BaseModel): audio_device_index: int = -1 # Default to -1 for system default or auto-detection sample_rate: int = 48000 # Default sample rate (BirdNET expects 48kHz) audio_channels: int = 1 # Default to mono (BirdNET processes mono audio) - analysis_overlap: float = 0.5 # Overlap in seconds between consecutive audio segments + audio_overlap: float = 0.5 # Overlap in seconds between consecutive audio segments # Logging settings logging: LoggingConfig = Field(default_factory=LoggingConfig) @@ -117,10 +117,6 @@ class BirdNETConfig(BaseModel): notify_quiet_hours_start: str = "" # "HH:MM" or empty for no quiet hours notify_quiet_hours_end: str = "" # "HH:MM" or empty for no quiet hours - # Flickr - flickr_api_key: str = "" - flickr_filter_email: str = "" - # Localization and Species Display language: str = "en" # Language code for UI and species name translation species_display_mode: str = "full" # Options: "full", "common_name", "scientific_name" @@ -129,12 +125,6 @@ class BirdNETConfig(BaseModel): # Field mode and GPS settings enable_gps: bool = False # Enable GPS tracking for field deployments gps_update_interval: float = 5.0 # GPS update interval in seconds - hardware_check_interval: float = 10.0 # Hardware monitoring interval in seconds - - # Hardware monitoring settings - enable_audio_device_check: bool = True # Enable audio device monitoring - enable_system_resource_check: bool = True # Enable system resource monitoring - enable_gps_check: bool = False # Enable GPS device monitoring # MQTT Integration settings enable_mqtt: bool = False # Enable MQTT publishing diff --git a/src/birdnetpi/config/versions/v1_9_0.py b/src/birdnetpi/config/versions/v1_9_0.py index 7c976331..60ac48a1 100644 --- a/src/birdnetpi/config/versions/v1_9_0.py +++ b/src/birdnetpi/config/versions/v1_9_0.py @@ -53,9 +53,6 @@ def defaults(self) -> dict[str, Any]: "apprise_only_notify_species_names": "", "apprise_weekly_report": False, "minimum_time_limit": 0, - # Flickr - "flickr_api_key": "", - "flickr_filter_email": "", # Localization "language": "en", "species_display_mode": "full", diff --git a/src/birdnetpi/config/versions/v2_0_0.py b/src/birdnetpi/config/versions/v2_0_0.py index a8f614ec..1a100a82 100644 --- a/src/birdnetpi/config/versions/v2_0_0.py +++ b/src/birdnetpi/config/versions/v2_0_0.py @@ -42,6 +42,7 @@ def defaults(self) -> dict[str, Any]: "audio_device_index": -1, "sample_rate": 48000, "audio_channels": 1, + "audio_overlap": 0.5, # Enhanced Logging Configuration "logging": { "level": "INFO", @@ -61,9 +62,6 @@ def defaults(self) -> dict[str, Any]: "notification_rules": [], "notify_quiet_hours_start": "", "notify_quiet_hours_end": "", - # Flickr - "flickr_api_key": "", - "flickr_filter_email": "", # Localization and Species Display "language": "en", "species_display_mode": "full", @@ -71,11 +69,6 @@ def defaults(self) -> dict[str, Any]: # Field mode and GPS settings "enable_gps": False, "gps_update_interval": 5.0, - "hardware_check_interval": 10.0, - # Hardware monitoring settings - "enable_audio_device_check": True, - "enable_system_resource_check": True, - "enable_gps_check": False, # MQTT Integration settings "enable_mqtt": False, "mqtt_broker_host": "localhost", diff --git a/src/birdnetpi/detections/models.py b/src/birdnetpi/detections/models.py index 7663a771..8ffa34e4 100644 --- a/src/birdnetpi/detections/models.py +++ b/src/birdnetpi/detections/models.py @@ -183,7 +183,10 @@ def serialize_model(self, serializer: object, info: object) -> dict[str, Any]: # Create a display service with the config and use it display_service = SpeciesDisplayService(config) # Always prefer translation (as recommended) - display_name = display_service.format_species_display(self, prefer_translation=True) + # Use format_full_species_display to respect species_display_mode setting + display_name = display_service.format_full_species_display( + self, prefer_translation=True + ) # Set both display_name and common_name for backward compatibility data["display_name"] = display_name diff --git a/src/birdnetpi/utils/helpers.py b/src/birdnetpi/utils/helpers.py new file mode 100644 index 00000000..1458329f --- /dev/null +++ b/src/birdnetpi/utils/helpers.py @@ -0,0 +1,28 @@ +"""General utility helper functions.""" + +from typing import TypeVar + +T = TypeVar("T") + + +def prefer(new_value: T | None, fallback: T) -> T: + """Return new_value if not None, otherwise return fallback. + + Useful for merging optional form fields with current config values. + + Args: + new_value: The potentially updated value + fallback: The fallback value to use if new_value is None + + Returns: + new_value if it's not None, otherwise fallback + + Example: + >>> prefer("updated", "original") + 'updated' + >>> prefer(None, "original") + 'original' + >>> prefer(False, True) # Works with falsy values + False + """ + return new_value if new_value is not None else fallback diff --git a/src/birdnetpi/web/routers/settings_view_routes.py b/src/birdnetpi/web/routers/settings_view_routes.py index da0c51cc..ca7d4843 100644 --- a/src/birdnetpi/web/routers/settings_view_routes.py +++ b/src/birdnetpi/web/routers/settings_view_routes.py @@ -20,6 +20,7 @@ from birdnetpi.system.log_reader import LogReaderService from birdnetpi.system.path_resolver import PathResolver from birdnetpi.system.status import SystemInspector +from birdnetpi.utils.helpers import prefer from birdnetpi.utils.language import get_user_language from birdnetpi.web.core.container import Container from birdnetpi.web.models.detections import DetectionEvent @@ -128,16 +129,13 @@ async def post_settings_view( audio_device_index: int = Form(...), sample_rate: int = Form(...), audio_channels: int = Form(...), - analysis_overlap: float = Form(...), + audio_overlap: float = Form(...), # External Services (optional - preserves existing if not provided) birdweather_id: str | None = Form(None), # New notification fields (JSON) - optional apprise_targets_json: str | None = Form(None), webhook_targets_json: str | None = Form(None), notification_rules_json: str | None = Form(None), - # Flickr (optional) - flickr_api_key: str | None = Form(None), - flickr_filter_email: str | None = Form(None), # Localization (optional) language: str | None = Form(None), species_display_mode: str | None = Form(None), @@ -145,10 +143,6 @@ async def post_settings_view( # Field Mode and GPS (optional) enable_gps: bool | None = Form(None), gps_update_interval: float | None = Form(None), - hardware_check_interval: float | None = Form(None), - enable_audio_device_check: bool | None = Form(None), - enable_system_resource_check: bool | None = Form(None), - enable_gps_check: bool | None = Form(None), # Analysis (optional) privacy_threshold: float | None = Form(None), # MQTT Integration (optional) @@ -227,11 +221,9 @@ async def post_settings_view( audio_device_index=audio_device_index, sample_rate=sample_rate, audio_channels=audio_channels, - analysis_overlap=analysis_overlap, + audio_overlap=audio_overlap, # External Services (preserve if not provided) - birdweather_id=birdweather_id - if birdweather_id is not None - else current_config.birdweather_id, + birdweather_id=prefer(birdweather_id, current_config.birdweather_id), # New Notification System (preserve if not provided) apprise_targets=apprise_targets, webhook_targets=webhook_targets, @@ -240,64 +232,27 @@ async def post_settings_view( notification_body_default=current_config.notification_body_default, notify_quiet_hours_start=current_config.notify_quiet_hours_start, notify_quiet_hours_end=current_config.notify_quiet_hours_end, - # Flickr (preserve if not provided) - flickr_api_key=flickr_api_key - if flickr_api_key is not None - else current_config.flickr_api_key, - flickr_filter_email=flickr_filter_email - if flickr_filter_email is not None - else current_config.flickr_filter_email, # Localization (preserve if not provided) - language=language if language is not None else current_config.language, - species_display_mode=species_display_mode - if species_display_mode is not None - else current_config.species_display_mode, - timezone=timezone if timezone is not None else current_config.timezone, + language=prefer(language, current_config.language), + species_display_mode=prefer(species_display_mode, current_config.species_display_mode), + timezone=prefer(timezone, current_config.timezone), # Field Mode and GPS (preserve if not provided) - enable_gps=enable_gps if enable_gps is not None else current_config.enable_gps, - gps_update_interval=gps_update_interval - if gps_update_interval is not None - else current_config.gps_update_interval, - hardware_check_interval=hardware_check_interval - if hardware_check_interval is not None - else current_config.hardware_check_interval, - enable_audio_device_check=enable_audio_device_check - if enable_audio_device_check is not None - else current_config.enable_audio_device_check, - enable_system_resource_check=enable_system_resource_check - if enable_system_resource_check is not None - else current_config.enable_system_resource_check, - enable_gps_check=enable_gps_check - if enable_gps_check is not None - else current_config.enable_gps_check, + enable_gps=prefer(enable_gps, current_config.enable_gps), + gps_update_interval=prefer(gps_update_interval, current_config.gps_update_interval), # Analysis (preserve if not provided) - privacy_threshold=privacy_threshold - if privacy_threshold is not None - else current_config.privacy_threshold, + privacy_threshold=prefer(privacy_threshold, current_config.privacy_threshold), # MQTT Integration (preserve if not provided) - enable_mqtt=enable_mqtt if enable_mqtt is not None else current_config.enable_mqtt, - mqtt_broker_host=mqtt_broker_host - if mqtt_broker_host is not None - else current_config.mqtt_broker_host, - mqtt_broker_port=mqtt_broker_port - if mqtt_broker_port is not None - else current_config.mqtt_broker_port, - mqtt_username=mqtt_username if mqtt_username is not None else current_config.mqtt_username, - mqtt_password=mqtt_password if mqtt_password is not None else current_config.mqtt_password, - mqtt_topic_prefix=mqtt_topic_prefix - if mqtt_topic_prefix is not None - else current_config.mqtt_topic_prefix, - mqtt_client_id=mqtt_client_id - if mqtt_client_id is not None - else current_config.mqtt_client_id, + enable_mqtt=prefer(enable_mqtt, current_config.enable_mqtt), + mqtt_broker_host=prefer(mqtt_broker_host, current_config.mqtt_broker_host), + mqtt_broker_port=prefer(mqtt_broker_port, current_config.mqtt_broker_port), + mqtt_username=prefer(mqtt_username, current_config.mqtt_username), + mqtt_password=prefer(mqtt_password, current_config.mqtt_password), + mqtt_topic_prefix=prefer(mqtt_topic_prefix, current_config.mqtt_topic_prefix), + mqtt_client_id=prefer(mqtt_client_id, current_config.mqtt_client_id), # Webhook Integration (preserve if not provided) - enable_webhooks=enable_webhooks - if enable_webhooks is not None - else current_config.enable_webhooks, + enable_webhooks=prefer(enable_webhooks, current_config.enable_webhooks), webhook_urls=webhook_urls_list, - webhook_events=webhook_events - if webhook_events is not None - else current_config.webhook_events, + webhook_events=prefer(webhook_events, current_config.webhook_events), # Update settings (always preserved from current config) updates=current_config.updates, ) diff --git a/src/birdnetpi/web/static/css/style.css b/src/birdnetpi/web/static/css/style.css index 8bd7f107..f4189414 100644 --- a/src/birdnetpi/web/static/css/style.css +++ b/src/birdnetpi/web/static/css/style.css @@ -901,7 +901,12 @@ h1 { font-weight: 500; } -/* Coordinate input */ +/* Coordinate section wrapper */ +.coordinate-section { + width: 100%; +} + +/* Coordinate input fields */ .coordinate-input { display: grid; grid-template-columns: 1fr 1fr; @@ -909,6 +914,21 @@ h1 { max-width: 400px; } +/* Make coordinate fields fit nicely */ +.coordinate-input > .coord-field { + max-width: 200px; +} + +/* Map widget takes full width of coordinate-section */ +.coordinate-section > .location-map-widget { + width: 100%; +} + +/* Coordinate inputs after map - no extra margin needed */ +.coordinate-section > .coordinate-input { + margin-top: 0; +} + .coord-field { display: flex; flex-direction: column; diff --git a/src/birdnetpi/web/templates/admin/settings.html.j2 b/src/birdnetpi/web/templates/admin/settings.html.j2 index 48582562..0e0a16b7 100644 --- a/src/birdnetpi/web/templates/admin/settings.html.j2 +++ b/src/birdnetpi/web/templates/admin/settings.html.j2 @@ -1,5 +1,12 @@ {% extends "base.html.j2" %} +{% block styles %} + + +{% endblock %} + {% block content %}