Vogelalbum 1.0.0
Eine Kamera am Vogelhaus, ein Rechner der erkennt wer da war — und eine Sammlung, die sich von allein fuellt. Zwei neuronale Netze: eines findet den Vogel im Bild, eines bestimmt die Art. 148 heimische Arten, auf Deutschland eingeschraenkt und neu normiert — ohne diese Einschraenkung wird aus einer Kohlmeise eine nordamerikanische Schwesterart. Daraus entsteht ein Sammelalbum: nummerierte Seiten, gefundene Arten in Farbe, fehlende als Schatten, dazu Erstfang, Punkte, Stufen und 15 Abzeichen. Laeuft vollstaendig auf eigener Hardware. Kein Cloud-Dienst, kein Konto, keine Telemetrie; die Bilder verlassen das eigene Netz nicht. Zwei getrennt deploybare Teile: die Sammlung als Django-Anwendung, die Erkennung als eigener Dienst. Der Erkennungs-Dienst fragt selbst an und braucht deshalb weder eine erreichbare Adresse noch einen offenen Port — er laeuft hinter einem gewoehnlichen Router. Dabei: Firmware fuer Raspberry Pi und ESP32-S3, Anbindung an Home Assistant ueber MQTT, Oberflaeche auf Deutsch und Englisch, Einstellungen je Kamera aus dem Browser, und ein Weg fuer Fotos von unterwegs. AGPL-3.0.
This commit is contained in:
commit
c11fe9ab04
189 files changed
+29806
No files matched your search
@@ -0,0 +1,118 @@
|
||||
# Generated by Django 6.1 on 2026-08-28 09:08
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Badge',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.SlugField(max_length=48, unique=True)),
|
||||
('name', models.CharField(max_length=80)),
|
||||
('description', models.CharField(max_length=200)),
|
||||
('emoji', models.CharField(default='🏅', max_length=8)),
|
||||
('xp', models.PositiveIntegerField(default=50)),
|
||||
('sort', models.PositiveIntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Abzeichen',
|
||||
'verbose_name_plural': 'Abzeichen',
|
||||
'ordering': ['sort', 'name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Sighting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('confidence', models.FloatField(default=0.0)),
|
||||
('started_at', models.DateTimeField(db_index=True)),
|
||||
('ended_at', models.DateTimeField()),
|
||||
('image', models.ImageField(blank=True, upload_to='sightings/%Y/%m/')),
|
||||
('crop', models.ImageField(blank=True, upload_to='crops/%Y/%m/')),
|
||||
('frames_total', models.PositiveIntegerField(default=0)),
|
||||
('frames_with_bird', models.PositiveIntegerField(default=0)),
|
||||
('votes', models.JSONField(blank=True, default=dict)),
|
||||
('mean_bird_score', models.FloatField(default=0.0)),
|
||||
('is_first_of_species', models.BooleanField(default=False)),
|
||||
('xp_awarded', models.PositiveIntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Sichtung',
|
||||
'verbose_name_plural': 'Sichtungen',
|
||||
'ordering': ['-started_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Species',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', models.SlugField(max_length=64, unique=True)),
|
||||
('german', models.CharField(max_length=80, verbose_name='Deutscher Name')),
|
||||
('scientific', models.CharField(max_length=120, verbose_name='Wissenschaftlicher Name')),
|
||||
('english', models.CharField(blank=True, max_length=120, verbose_name='Englischer Name')),
|
||||
('rarity', models.CharField(choices=[('haeufig', 'Häufig'), ('gelegentlich', 'Gelegentlich'), ('selten', 'Selten'), ('episch', 'Episch'), ('legendaer', 'Legendär')], max_length=16)),
|
||||
('xp', models.PositiveIntegerField(default=10)),
|
||||
('fact', models.TextField(blank=True, verbose_name='Steckbrief')),
|
||||
('size_cm', models.CharField(blank=True, max_length=16)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Art',
|
||||
'verbose_name_plural': 'Arten',
|
||||
'ordering': ['german'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='EarnedBadge',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('earned_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('badge', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='earned', to='dex.badge')),
|
||||
('sighting', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dex.sighting')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Verdientes Abzeichen',
|
||||
'verbose_name_plural': 'Verdiente Abzeichen',
|
||||
'ordering': ['-earned_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='confirmed_species',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confirmations', to='dex.species', verbose_name='Von Hand bestätigt als'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='species',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sightings', to='dex.species'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DexEntry',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('first_seen', models.DateTimeField()),
|
||||
('last_seen', models.DateTimeField()),
|
||||
('count', models.PositiveIntegerField(default=0)),
|
||||
('best_sighting', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dex.sighting')),
|
||||
('species', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='entry', to='dex.species')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Dex-Eintrag',
|
||||
'verbose_name_plural': 'Dex-Einträge',
|
||||
'ordering': ['-count'],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='sighting',
|
||||
index=models.Index(fields=['-started_at', 'species'], name='dex_sightin_started_7b6a20_idx'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 6.1 on 2026-08-28 09:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='reference_credit',
|
||||
field=models.CharField(blank=True, max_length=250, verbose_name='Urheber'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='reference_image',
|
||||
field=models.ImageField(blank=True, upload_to='referenz/', verbose_name='Referenzfoto'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='reference_license',
|
||||
field=models.CharField(blank=True, max_length=80, verbose_name='Lizenz'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='reference_source',
|
||||
field=models.URLField(blank=True, max_length=400, verbose_name='Quelle'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 6.1 on 2026-08-28 11:11
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0002_species_reference_credit_species_reference_image_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Camera',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=80)),
|
||||
('slug', models.SlugField(max_length=48, unique=True)),
|
||||
('location', models.CharField(blank=True, max_length=120, verbose_name='Standort')),
|
||||
('token_hash', models.CharField(editable=False, max_length=64, unique=True)),
|
||||
('active', models.BooleanField(default=True, verbose_name='Aktiv')),
|
||||
('created_at', models.DateTimeField(default=django.utils.timezone.now)),
|
||||
('last_seen', models.DateTimeField(blank=True, null=True, verbose_name='Zuletzt gemeldet')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Kamera',
|
||||
'verbose_name_plural': 'Kameras',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='camera',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sightings', to='dex.camera', verbose_name='Kamera'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,77 @@
|
||||
# Generated by Django 6.1 on 2026-08-28 15:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0003_camera_sighting_camera'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='species',
|
||||
options={'ordering': ['number', 'german'], 'verbose_name': 'Art', 'verbose_name_plural': 'Arten'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='beschreibung',
|
||||
field=models.TextField(blank=True, verbose_name='Beschreibung'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='gewicht_g',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Gewicht (g)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='kategorie',
|
||||
field=models.CharField(blank=True, max_length=80, verbose_name='Kategorie'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='lebensraum',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='Lebensraum'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='nahrung',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='Nahrung'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='number',
|
||||
field=models.PositiveIntegerField(db_index=True, default=0, verbose_name='Dex-Nummer'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='spannweite_cm',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Spannweite (cm)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='stimme',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='Stimme'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='typ',
|
||||
field=models.CharField(blank=True, max_length=24, verbose_name='Typ'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='verwechslung',
|
||||
field=models.JSONField(blank=True, default=list, verbose_name='Verwechslungsgefahr'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='zug',
|
||||
field=models.CharField(blank=True, max_length=24, verbose_name='Zugverhalten'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='species',
|
||||
name='fact',
|
||||
field=models.TextField(blank=True, verbose_name='Kurzfakt'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 6.1 on 2026-08-28 16:01
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0004_alter_species_options_species_beschreibung_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='beschreibung_en',
|
||||
field=models.TextField(blank=True, verbose_name='Beschreibung (en)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='kategorie_en',
|
||||
field=models.CharField(blank=True, max_length=80, verbose_name='Kategorie (en)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='lebensraum_en',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='Lebensraum (en)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='nahrung_en',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='Nahrung (en)'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='stimme_en',
|
||||
field=models.CharField(blank=True, max_length=200, verbose_name='Stimme (en)'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.1 on 2026-08-28 16:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0005_species_beschreibung_en_species_kategorie_en_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='lebensraum_typ',
|
||||
field=models.CharField(db_index=True, default='garten', max_length=16, verbose_name='Lebensraum-Gruppe'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='species',
|
||||
name='regions',
|
||||
field=models.JSONField(blank=True, default=list, verbose_name='Regionen'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.1 on 2026-09-12 12:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0006_species_lebensraum_typ_species_regions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='live_enabled',
|
||||
field=models.BooleanField(default=False, help_text='Zeigt das Livebild dieser Kamera auf der Seite /live/. Bewusst standardmäßig aus: ein Livebild aus dem eigenen Garten gehört nicht auf eine öffentliche Seite.', verbose_name='Live-Bild zeigen'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='live_url',
|
||||
field=models.URLField(blank=True, help_text='MJPEG-Adresse des Live-Verteilers, z.B. http://erkennung.local:8131/live — NICHT die Kamera selbst, sonst fehlt der Erkennung das Bild.', verbose_name='Live-Adresse'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 6.1 on 2026-09-12 13:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0007_camera_live_enabled_camera_live_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='detect_threshold',
|
||||
field=models.FloatField(default=0.4, help_text='Ab welcher Sicherheit etwas als Vogel gilt (0–1).', verbose_name='Schwelle Vogelerkennung'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='habitats',
|
||||
field=models.CharField(default='garten', help_text='Kommagetrennt: garten, wald, offenland, wasser, gebirge. Schränkt ein, welche Arten der Klassifikator überhaupt erwägt — eine Kamera am Futterhaus soll gar nicht erst überlegen, ob da ein Kranich sitzt. Leer lässt alle zu.', max_length=120, verbose_name='Lebensräume'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='min_bird_score',
|
||||
field=models.FloatField(default=0.05, help_text='Darunter gilt die Sichtung als unbestimmt. Gemessen ist 0.05 richtig: bei 0.10 fliegen richtige Bestimmungen raus, weil bei manchen Arten global die amerikanische Schwesterart gewinnt.', verbose_name='Mindestanteil heimischer Arten'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='stream_url',
|
||||
field=models.URLField(blank=True, help_text='MJPEG- oder Snapshot-Adresse der Kamera, z.B. http://vogelkamera.local:8080/stream. Leer heißt: der Detector nimmt, was auf seiner Kommandozeile steht.', verbose_name='Stream-Adresse'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,29 @@
|
||||
# Generated by Django 6.1 on 2026-09-23 20:14
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0008_camera_detect_threshold_camera_habitats_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Benachrichtigung',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('aktiv', models.BooleanField(default=False, verbose_name='Meldungen einschalten')),
|
||||
('server', models.URLField(default='https://ntfy.sh', help_text='Eigener Server oder ntfy.sh', verbose_name='ntfy-Server')),
|
||||
('thema', models.CharField(blank=True, help_text='Der Name, den du in der ntfy-App abonnierst. ⚠ Wer das Thema kennt, kann mitlesen — nimm etwas, das niemand errät.', max_length=100, verbose_name='Thema')),
|
||||
('sichtungen', models.CharField(choices=[('aus', 'keine Sichtungsmeldungen'), ('neue', 'nur neue Arten (Erstfang)'), ('alle', 'alle Sichtungen')], default='neue', help_text="Ein gut besuchtes Futterhaus erzeugt schnell Dutzende Meldungen am Tag. 'Nur neue Arten' bleibt dauerhaft interessant.", max_length=8, verbose_name='Sichtungen')),
|
||||
('stoerungen', models.BooleanField(default=True, help_text='Kamera meldet sich nicht mehr, Erkennung steht.', verbose_name='Störungen melden')),
|
||||
('stille_minuten', models.PositiveIntegerField(default=30, help_text='So lange darf eine Kamera schweigen, bevor es eine Meldung gibt. Zu knapp bedeutet Fehlalarm bei jedem Router-Neustart.', verbose_name='Störung nach … Minuten Stille')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Benachrichtigung',
|
||||
'verbose_name_plural': 'Benachrichtigungen',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.1 on 2026-09-23 22:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0009_benachrichtigung'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='letzter_kontakt',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Letztes Lebenszeichen'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.1 on 2026-09-24 05:19
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0010_camera_letzter_kontakt'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='live_bildrate',
|
||||
field=models.FloatField(default=2.0, help_text='Gilt NUR für Zuschauer — die Erkennung bekommt immer jedes Bild. Gemessen am 24.09.2026: ohne Begrenzung schickt eine Kamera in HD rund 6 bis 10 Mbit/s. Zwei Kameras nebeneinander sind über 16 Mbit/s, und auf dem Handy dauert es entsprechend, bis das erste Bild erscheint. 2 Bilder/s sind flüssig genug fürs Zuschauen und brauchen etwa ein Achtel davon. Höher setzen heißt: ruckelfreier, aber träger Start und mehr Funklast. 0 schaltet die Begrenzung ab.', verbose_name='Bilder je Sekunde in der Vorschau'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.1 on 2026-10-01 06:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0011_camera_live_bildrate'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='stoerung_gemeldet_seit',
|
||||
field=models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Störung gemeldet seit'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.1 on 2026-10-01 12:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0012_camera_stoerung_gemeldet_seit'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='latitude',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Breitengrad'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='longitude',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Längengrad'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 6.1 on 2026-10-01 12:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0013_sighting_latitude_sighting_longitude'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='benachrichtigung',
|
||||
name='fotos_gemeldet_seit',
|
||||
field=models.DateTimeField(blank=True, editable=False, null=True, verbose_name='Wartende Fotos gemeldet seit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='mobil',
|
||||
field=models.BooleanField(default=False, help_text='Lädt Fotos hoch statt zu streamen (Handy, Tablet). Solche Geräte werden nicht überwacht — sie melden sich nur, wenn jemand unterwegs fotografiert.', verbose_name='Mobiles Gerät'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='wartet_auf_bestimmung',
|
||||
field=models.BooleanField(db_index=True, default=False, verbose_name='Wartet auf Bestimmung'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.1 on 2026-10-01 14:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0014_benachrichtigung_fotos_gemeldet_seit_camera_mobil_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='camera',
|
||||
name='min_mean_bird_score',
|
||||
field=models.FloatField(default=0.0, help_text='Liegt der mittlere Vogelwert eines Besuchs darunter, entsteht gar keine Sichtung. 0 heißt: nur Meldungen ohne jeden Vogelwert werden abgewiesen. Erst hochdrehen, wenn genug eigene Sichtungen vorliegen — sonst wirft man richtige Bestimmungen weg.', verbose_name='Mindest-Vogelwert über den Besuch'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,33 @@
|
||||
# Generated by Django 6.1 on 2026-10-14 18:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dex', '0015_camera_min_mean_bird_score'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='box_x0',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Vogel: links'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='box_x1',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Vogel: rechts'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='box_y0',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Vogel: oben'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sighting',
|
||||
name='box_y1',
|
||||
field=models.FloatField(blank=True, null=True, verbose_name='Vogel: unten'),
|
||||
),
|
||||
]
|
||||
Whitespace-only changes.
Reference in new issue
Block a user