Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions config_templates/birdnetpi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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

Expand Down
8 changes: 4 additions & 4 deletions src/birdnetpi/audio/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
12 changes: 1 addition & 11 deletions src/birdnetpi/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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"
Expand All @@ -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
Expand Down
3 changes: 0 additions & 3 deletions src/birdnetpi/config/versions/v1_9_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 1 addition & 8 deletions src/birdnetpi/config/versions/v2_0_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -61,21 +62,13 @@ 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",
"timezone": "UTC",
# 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",
Expand Down
5 changes: 4 additions & 1 deletion src/birdnetpi/detections/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 28 additions & 0 deletions src/birdnetpi/utils/helpers.py
Original file line number Diff line number Diff line change
@@ -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
83 changes: 19 additions & 64 deletions src/birdnetpi/web/routers/settings_view_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -128,27 +129,20 @@ 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),
timezone: str | None = Form(None),
# 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)
Expand Down Expand Up @@ -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,
Expand All @@ -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,
)
Expand Down
22 changes: 21 additions & 1 deletion src/birdnetpi/web/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -901,14 +901,34 @@ 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;
gap: 1rem;
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;
Expand Down
Loading
Loading