Add plotter and fix miner
This commit is contained in:
parent
d56f3806fa
commit
acc51fdfc4
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,4 +8,4 @@ species_list.txt
|
|||||||
|
|
||||||
push.sh
|
push.sh
|
||||||
|
|
||||||
config/analyzer.conf
|
config/*.conf
|
@ -1,35 +0,0 @@
|
|||||||
# !/bin/bash
|
|
||||||
|
|
||||||
# Extract data generated with BirdNET on record to get relevant informations and record data in sqlite
|
|
||||||
|
|
||||||
# Load config file
|
|
||||||
config_filepath="./config/analyzer.conf"
|
|
||||||
if [ -f "$config_filepath" ]; then
|
|
||||||
source "$config_filepath"
|
|
||||||
else
|
|
||||||
echo "Config file not found: $config_filepath"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Verify needed prerequisites
|
|
||||||
if [[ -z ${CHUNK_FOLDER} ]]; then
|
|
||||||
echo "CHUNK_FOLDER is not set"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
if [[ ! -d "${CHUNK_FOLDER}" ]]; then
|
|
||||||
echo "CHUNK_FOLDER does not exist: ${CHUNK_FOLDER}"
|
|
||||||
exit 1
|
|
||||||
else
|
|
||||||
if [[ ! -d "${CHUNK_FOLDER}/out" ]]; then
|
|
||||||
echo "Output dir does not exist: ${CHUNK_FOLDER}/out"
|
|
||||||
echo "Cannot mine data"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
function list_all_model_outputs()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
2
.ideas/journal.php
Executable file
2
.ideas/journal.php
Executable file
@ -0,0 +1,2 @@
|
|||||||
|
<?php
|
||||||
|
echo shell_exec("journalctl -u birdnet_recording -n 10");
|
3
.ideas/url_escape.php
Normal file
3
.ideas/url_escape.php
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
echo urlencode("contact@ortion.fr");
|
3
TODO
3
TODO
@ -1 +1,4 @@
|
|||||||
- Fix install of venv
|
- Fix install of venv
|
||||||
|
- Fix clean script
|
||||||
|
- Change install script for php 8.1
|
||||||
|
- Fix service manager
|
||||||
|
@ -10,6 +10,7 @@ SPECIES_LIST="./config/species_list.txt"
|
|||||||
CONFIDENCE=0.1
|
CONFIDENCE=0.1
|
||||||
# Recording duration (in seconds)
|
# Recording duration (in seconds)
|
||||||
RECORDING_DURATION=15
|
RECORDING_DURATION=15
|
||||||
|
RECORDING_AMPLIFICATION=1.5
|
||||||
# Chunk folder location
|
# Chunk folder location
|
||||||
CHUNK_FOLDER="./var/chunks"
|
CHUNK_FOLDER="./var/chunks"
|
||||||
# Audio recording device (pulseaudio)
|
# Audio recording device (pulseaudio)
|
||||||
@ -18,3 +19,10 @@ AUDIO_DEVICE="default"
|
|||||||
PYTHON_VENV="./.venv/birdnet-stream"
|
PYTHON_VENV="./.venv/birdnet-stream"
|
||||||
# Database location
|
# Database location
|
||||||
DATABASE="./var/db.sqlite"
|
DATABASE="./var/db.sqlite"
|
||||||
|
|
||||||
|
DAEMON_USER="ortion"
|
||||||
|
DAEMON_PASSWORD="41uDIAh8"
|
||||||
|
|
||||||
|
NOTIFY_XMPP_SERVER="chapril.org"
|
||||||
|
NOTIFY_XMPP_USER="samulus.i.n"
|
||||||
|
PASSWORD="elathous730"
|
||||||
|
@ -1,189 +0,0 @@
|
|||||||
Acanthis cabaret_Lesser Redpoll
|
|
||||||
Accipiter nisus_Eurasian Sparrowhawk
|
|
||||||
Acrocephalus palustris_Marsh Warbler
|
|
||||||
Acrocephalus schoenobaenus_Sedge Warbler
|
|
||||||
Acrocephalus scirpaceus_Eurasian Reed Warbler
|
|
||||||
Actitis hypoleucos_Common Sandpiper
|
|
||||||
Aegithalos caudatus_Long-tailed Tit
|
|
||||||
Aix galericulata_Mandarin Duck
|
|
||||||
Alauda arvensis_Eurasian Skylark
|
|
||||||
Alcedo atthis_Common Kingfisher
|
|
||||||
Alectoris rufa_Red-legged Partridge
|
|
||||||
Alopochen aegyptiaca_Egyptian Goose
|
|
||||||
Anas acuta_Northern Pintail
|
|
||||||
Anas crecca_Green-winged Teal
|
|
||||||
Anas platyrhynchos_Mallard
|
|
||||||
Anser albifrons_Greater White-fronted Goose
|
|
||||||
Anser anser_Graylag Goose
|
|
||||||
Anthus petrosus_Rock Pipit
|
|
||||||
Anthus pratensis_Meadow Pipit
|
|
||||||
Anthus spinoletta_Water Pipit
|
|
||||||
Anthus trivialis_Tree Pipit
|
|
||||||
Apus apus_Common Swift
|
|
||||||
Aquila chrysaetos_Golden Eagle
|
|
||||||
Ardea alba_Great Egret
|
|
||||||
Ardea cinerea_Gray Heron
|
|
||||||
Ardea purpurea_Purple Heron
|
|
||||||
Arenaria interpres_Ruddy Turnstone
|
|
||||||
Aythya ferina_Common Pochard
|
|
||||||
Aythya fuligula_Tufted Duck
|
|
||||||
Aythya marila_Greater Scaup
|
|
||||||
Branta bernicla_Brant
|
|
||||||
Branta canadensis_Canada Goose
|
|
||||||
Bubulcus ibis_Cattle Egret
|
|
||||||
Bucephala clangula_Common Goldeneye
|
|
||||||
Buteo buteo_Common Buzzard
|
|
||||||
Calidris alpina_Dunlin
|
|
||||||
Calidris melanotos_Pectoral Sandpiper
|
|
||||||
Calidris pugnax_Ruff
|
|
||||||
Carduelis carduelis_European Goldfinch
|
|
||||||
Certhia brachydactyla_Short-toed Treecreeper
|
|
||||||
Certhia familiaris_Eurasian Treecreeper
|
|
||||||
Cettia cetti_Cetti's Warbler
|
|
||||||
Charadrius dubius_Little Ringed Plover
|
|
||||||
Charadrius hiaticula_Common Ringed Plover
|
|
||||||
Chlidonias hybrida_Whiskered Tern
|
|
||||||
Chloris chloris_European Greenfinch
|
|
||||||
Chroicocephalus ridibundus_Black-headed Gull
|
|
||||||
Ciconia ciconia_White Stork
|
|
||||||
Cinclus cinclus_White-throated Dipper
|
|
||||||
Circaetus gallicus_Short-toed Snake-Eagle
|
|
||||||
Circus aeruginosus_Eurasian Marsh-Harrier
|
|
||||||
Circus pygargus_Montagu's Harrier
|
|
||||||
Cisticola juncidis_Zitting Cisticola
|
|
||||||
Coccothraustes coccothraustes_Hawfinch
|
|
||||||
Columba livia_Rock Pigeon
|
|
||||||
Columba oenas_Stock Dove
|
|
||||||
Columba palumbus_Common Wood-Pigeon
|
|
||||||
Corvus corax_Common Raven
|
|
||||||
Corvus corone_Carrion Crow
|
|
||||||
Corvus frugilegus_Rook
|
|
||||||
Corvus monedula_Eurasian Jackdaw
|
|
||||||
Coturnix coturnix_Common Quail
|
|
||||||
Cuculus canorus_Common Cuckoo
|
|
||||||
Curruca communis_Greater Whitethroat
|
|
||||||
Curruca curruca_Lesser Whitethroat
|
|
||||||
Curruca undata_Dartford Warbler
|
|
||||||
Cyanistes caeruleus_Eurasian Blue Tit
|
|
||||||
Cygnus olor_Mute Swan
|
|
||||||
Delichon urbicum_Common House-Martin
|
|
||||||
Dendrocopos major_Great Spotted Woodpecker
|
|
||||||
Dendrocoptes medius_Middle Spotted Woodpecker
|
|
||||||
Dryobates minor_Lesser Spotted Woodpecker
|
|
||||||
Dryocopus martius_Black Woodpecker
|
|
||||||
Egretta garzetta_Little Egret
|
|
||||||
Emberiza calandra_Corn Bunting
|
|
||||||
Emberiza cirlus_Cirl Bunting
|
|
||||||
Emberiza citrinella_Yellowhammer
|
|
||||||
Emberiza schoeniclus_Reed Bunting
|
|
||||||
Erithacus rubecula_European Robin
|
|
||||||
Falco peregrinus_Peregrine Falcon
|
|
||||||
Falco subbuteo_Eurasian Hobby
|
|
||||||
Falco tinnunculus_Eurasian Kestrel
|
|
||||||
Ficedula hypoleuca_European Pied Flycatcher
|
|
||||||
Fringilla coelebs_Common Chaffinch
|
|
||||||
Fringilla montifringilla_Brambling
|
|
||||||
Fulica atra_Eurasian Coot
|
|
||||||
Gallinago gallinago_Common Snipe
|
|
||||||
Gallinula chloropus_Eurasian Moorhen
|
|
||||||
Garrulus glandarius_Eurasian Jay
|
|
||||||
Grus grus_Common Crane
|
|
||||||
Haematopus ostralegus_Eurasian Oystercatcher
|
|
||||||
Himantopus himantopus_Black-winged Stilt
|
|
||||||
Hippolais polyglotta_Melodious Warbler
|
|
||||||
Hirundo rustica_Barn Swallow
|
|
||||||
Ichthyaetus melanocephalus_Mediterranean Gull
|
|
||||||
Lanius collurio_Red-backed Shrike
|
|
||||||
Larus argentatus_Herring Gull
|
|
||||||
Larus canus_Common Gull
|
|
||||||
Larus fuscus_Lesser Black-backed Gull
|
|
||||||
Larus marinus_Great Black-backed Gull
|
|
||||||
Larus michahellis_Yellow-legged Gull
|
|
||||||
Limosa lapponica_Bar-tailed Godwit
|
|
||||||
Limosa limosa_Black-tailed Godwit
|
|
||||||
Linaria cannabina_Eurasian Linnet
|
|
||||||
Locustella naevia_Common Grasshopper-Warbler
|
|
||||||
Lophophanes cristatus_Crested Tit
|
|
||||||
Loxia curvirostra_Red Crossbill
|
|
||||||
Lullula arborea_Wood Lark
|
|
||||||
Luscinia megarhynchos_Common Nightingale
|
|
||||||
Luscinia svecica_Bluethroat
|
|
||||||
Mareca penelope_Eurasian Wigeon
|
|
||||||
Mareca strepera_Gadwall
|
|
||||||
Mergus merganser_Common Merganser
|
|
||||||
Milvus migrans_Black Kite
|
|
||||||
Milvus milvus_Red Kite
|
|
||||||
Morus bassanus_Northern Gannet
|
|
||||||
Motacilla alba_White Wagtail
|
|
||||||
Motacilla cinerea_Gray Wagtail
|
|
||||||
Motacilla flava_Western Yellow Wagtail
|
|
||||||
Muscicapa striata_Spotted Flycatcher
|
|
||||||
Numenius arquata_Eurasian Curlew
|
|
||||||
Nycticorax nycticorax_Black-crowned Night-Heron
|
|
||||||
Oenanthe oenanthe_Northern Wheatear
|
|
||||||
Oriolus oriolus_Eurasian Golden Oriole
|
|
||||||
Pandion haliaetus_Osprey
|
|
||||||
Panurus biarmicus_Bearded Reedling
|
|
||||||
Parus major_Great Tit
|
|
||||||
Passer domesticus_House Sparrow
|
|
||||||
Passer montanus_Eurasian Tree Sparrow
|
|
||||||
Perdix perdix_Gray Partridge
|
|
||||||
Periparus ater_Coal Tit
|
|
||||||
Pernis apivorus_European Honey-buzzard
|
|
||||||
Phalacrocorax carbo_Great Cormorant
|
|
||||||
Phasianus colchicus_Ring-necked Pheasant
|
|
||||||
Phoenicurus ochruros_Black Redstart
|
|
||||||
Phoenicurus phoenicurus_Common Redstart
|
|
||||||
Phylloscopus bonelli_Western Bonelli's Warbler
|
|
||||||
Phylloscopus collybita_Common Chiffchaff
|
|
||||||
Phylloscopus ibericus_Iberian Chiffchaff
|
|
||||||
Phylloscopus trochilus_Willow Warbler
|
|
||||||
Pica pica_Eurasian Magpie
|
|
||||||
Picus viridis_Eurasian Green Woodpecker
|
|
||||||
Pluvialis apricaria_European Golden-Plover
|
|
||||||
Pluvialis squatarola_Black-bellied Plover
|
|
||||||
Podiceps cristatus_Great Crested Grebe
|
|
||||||
Poecile montanus_Willow Tit
|
|
||||||
Poecile palustris_Marsh Tit
|
|
||||||
Prunella modularis_Dunnock
|
|
||||||
Psittacula krameri_Rose-ringed Parakeet
|
|
||||||
Pyrrhocorax pyrrhocorax_Red-billed Chough
|
|
||||||
Pyrrhula pyrrhula_Eurasian Bullfinch
|
|
||||||
Rallus aquaticus_Water Rail
|
|
||||||
Recurvirostra avosetta_Pied Avocet
|
|
||||||
Regulus ignicapilla_Common Firecrest
|
|
||||||
Regulus regulus_Goldcrest
|
|
||||||
Remiz pendulinus_Eurasian Penduline-Tit
|
|
||||||
Riparia riparia_Bank Swallow
|
|
||||||
Saxicola rubetra_Whinchat
|
|
||||||
Saxicola rubicola_European Stonechat
|
|
||||||
Serinus serinus_European Serin
|
|
||||||
Sitta europaea_Eurasian Nuthatch
|
|
||||||
Spatula clypeata_Northern Shoveler
|
|
||||||
Spatula querquedula_Garganey
|
|
||||||
Spinus spinus_Eurasian Siskin
|
|
||||||
Sterna hirundo_Common Tern
|
|
||||||
Sternula albifrons_Little Tern
|
|
||||||
Streptopelia decaocto_Eurasian Collared-Dove
|
|
||||||
Streptopelia turtur_European Turtle-Dove
|
|
||||||
Strix aluco_Tawny Owl
|
|
||||||
Sturnus vulgaris_European Starling
|
|
||||||
Sylvia atricapilla_Eurasian Blackcap
|
|
||||||
Sylvia borin_Garden Warbler
|
|
||||||
Tachybaptus ruficollis_Little Grebe
|
|
||||||
Tadorna tadorna_Common Shelduck
|
|
||||||
Thalasseus sandvicensis_Sandwich Tern
|
|
||||||
Tringa erythropus_Spotted Redshank
|
|
||||||
Tringa glareola_Wood Sandpiper
|
|
||||||
Tringa nebularia_Common Greenshank
|
|
||||||
Tringa ochropus_Green Sandpiper
|
|
||||||
Tringa totanus_Common Redshank
|
|
||||||
Troglodytes troglodytes_Eurasian Wren
|
|
||||||
Turdus iliacus_Redwing
|
|
||||||
Turdus merula_Eurasian Blackbird
|
|
||||||
Turdus philomelos_Song Thrush
|
|
||||||
Turdus pilaris_Fieldfare
|
|
||||||
Turdus viscivorus_Mistle Thrush
|
|
||||||
Tyto alba_Barn Owl
|
|
||||||
Uria aalge_Common Murre
|
|
||||||
Vanellus vanellus_Northern Lapwing
|
|
@ -80,6 +80,8 @@ analyze_chunks() {
|
|||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_prerequisites
|
||||||
|
|
||||||
# Get list of current chunk in working directory
|
# Get list of current chunk in working directory
|
||||||
chunks=$(get_chunk_list)
|
chunks=$(get_chunk_list)
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#! /usr/bin/env bash
|
#! /usr/bin/env bash
|
||||||
|
# inspired by https://unix.stackexchange.com/questions/47132/execute-shell-script-from-php-as-root-user
|
||||||
set -e
|
set -e
|
||||||
# set -x
|
# set -x
|
||||||
|
|
||||||
@ -9,19 +9,19 @@ if [ -f "$config_filepath" ]; then
|
|||||||
source "$config_filepath"
|
source "$config_filepath"
|
||||||
else
|
else
|
||||||
echo "Config file not found: $config_filepath"
|
echo "Config file not found: $config_filepath"
|
||||||
exit 1
|
# exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z $DAEMON_USER ]]
|
if [[ -z $DAEMON_USER ]]
|
||||||
then
|
then
|
||||||
echo "DAEMON_USER is not set"
|
echo "DAEMON_USER is not set"
|
||||||
exit 1
|
# exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ -z $DAEMON_PASSWORD ]]
|
if [[ -z $DAEMON_PASSWORD ]]
|
||||||
then
|
then
|
||||||
echo "DAEMON_PASSWORD is not set"
|
echo "DAEMON_PASSWORD is not set"
|
||||||
exit 1
|
# exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SERVICES="$(sudo -S <<< $DAEMON_PASSWORD ls /etc/systemd/system/ | grep 'birdnet')"
|
SERVICES="$(sudo -S <<< $DAEMON_PASSWORD ls /etc/systemd/system/ | grep 'birdnet')"
|
||||||
@ -36,17 +36,15 @@ debug() {
|
|||||||
|
|
||||||
manage() {
|
manage() {
|
||||||
action=$1
|
action=$1
|
||||||
|
if [[ -z $2 ]]; then
|
||||||
|
services=$SERVICES
|
||||||
|
else
|
||||||
|
services=$2
|
||||||
|
fi
|
||||||
debug "$action birdnet services"
|
debug "$action birdnet services"
|
||||||
sudo -S <<< $DAEMON_PASSWORD systemctl $action $SERVICES
|
# sshpass -p $DAEMON_PASSWORD sudo -S -u $DAEMON_USER sudo systemctl $action $services
|
||||||
|
sudo systemctl $action $services
|
||||||
echo "done"
|
echo "done"
|
||||||
}
|
}
|
||||||
|
|
||||||
stop() {
|
manage $1 $2
|
||||||
manage stop
|
|
||||||
}
|
|
||||||
|
|
||||||
start() {
|
|
||||||
manage start
|
|
||||||
}
|
|
||||||
|
|
||||||
manage $1
|
|
@ -3,7 +3,6 @@
|
|||||||
#
|
#
|
||||||
|
|
||||||
DEBUG=${DEBUG:-1}
|
DEBUG=${DEBUG:-1}
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
# set -x
|
# set -x
|
||||||
|
|
||||||
@ -99,9 +98,8 @@ save_observations() {
|
|||||||
location_id=$(get_location_id "$LATITUDE" "$LONGITUDE")
|
location_id=$(get_location_id "$LATITUDE" "$LONGITUDE")
|
||||||
fi
|
fi
|
||||||
datetime=$(record_datetime $source_audio)
|
datetime=$(record_datetime $source_audio)
|
||||||
if [[ $(observation_exists "$source_audio" "$start" "$end" "$taxon_id" "$location_id") = "true" ]]; then
|
if [[ $(observation_exists "$source_audio" "$start" "$end" "$taxon_id" "$location_id") -eq 1 ]]; then
|
||||||
debug "Observation already exists: $source_audio, $start, $end, $taxon_id, $location_id"
|
debug "Observation already exists: $source_audio, $start, $end, $taxon_id, $location_id"
|
||||||
exit 1
|
|
||||||
else
|
else
|
||||||
debug "Inserting observation: $source_audio, $start, $end, $taxon_id, $location_id, $datetime"
|
debug "Inserting observation: $source_audio, $start, $end, $taxon_id, $location_id, $datetime"
|
||||||
insert_observation "$source_audio" "$start" "$end" "$taxon_id" "$location_id" "$confidence" "$datetime"
|
insert_observation "$source_audio" "$start" "$end" "$taxon_id" "$location_id" "$confidence" "$datetime"
|
||||||
|
@ -32,7 +32,7 @@ record() {
|
|||||||
DEVICE=$1
|
DEVICE=$1
|
||||||
DURATION=$2
|
DURATION=$2
|
||||||
debug "Recording from $DEVICE for $DURATION seconds"
|
debug "Recording from $DEVICE for $DURATION seconds"
|
||||||
ffmpeg -nostdin -f pulse -i ${DEVICE} -t ${DURATION} -vn -acodec pcm_s16le -ac 1 -ar 48000 file:${CHUNK_FOLDER}/in/birdnet_$(date "+%Y%m%d_%H%M%S").wav
|
ffmpeg -nostdin -hide_banner -loglevel error -nostats -f pulse -i ${DEVICE} -t ${DURATION} -vn -acodec pcm_s16le -ac 1 -ar 48000 -af "volume=$RECORDING_AMPLIFY" file:${CHUNK_FOLDER}/in/birdnet_$(date "+%Y%m%d_%H%M%S").wav
|
||||||
}
|
}
|
||||||
|
|
||||||
config_filepath="./config/analyzer.conf"
|
config_filepath="./config/analyzer.conf"
|
||||||
|
13
daemon/notify/apprise.sh
Executable file
13
daemon/notify/apprise.sh
Executable file
@ -0,0 +1,13 @@
|
|||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
send() {
|
||||||
|
message=$1
|
||||||
|
if [ -z "$message" ]; then
|
||||||
|
echo "No message to send"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
apprise -vv -t "BirdNET-stream" -b "$message" \
|
||||||
|
--config "./config/apprise.conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
send $1
|
0
daemon/plotter/__init__.py
Normal file
0
daemon/plotter/__init__.py
Normal file
120
daemon/plotter/chart.py
Executable file
120
daemon/plotter/chart.py
Executable file
@ -0,0 +1,120 @@
|
|||||||
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
|
from curses import def_prog_mode
|
||||||
|
import sqlite3
|
||||||
|
from xml.sax.handler import feature_external_ges
|
||||||
|
import pandas as pd
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.colors import LogNorm
|
||||||
|
import seaborn as sns
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
CONFIG = {
|
||||||
|
"readings": 10,
|
||||||
|
"palette": "Greens",
|
||||||
|
}
|
||||||
|
|
||||||
|
db = None
|
||||||
|
def get_database():
|
||||||
|
global db
|
||||||
|
if db is None:
|
||||||
|
db = sqlite3.connect('/home/ortion/Desktop/db.sqlite')
|
||||||
|
return db
|
||||||
|
|
||||||
|
|
||||||
|
def get_detection_hourly(date):
|
||||||
|
db = get_database()
|
||||||
|
df = pd.read_sql_query("""SELECT common_name, date, location_id, confidence
|
||||||
|
FROM observation
|
||||||
|
INNER JOIN taxon
|
||||||
|
ON observation.taxon_id = taxon.taxon_id""", db)
|
||||||
|
|
||||||
|
df['date'] = pd.to_datetime(df['date'])
|
||||||
|
df['hour'] = df['date'].dt.hour
|
||||||
|
df['date'] = df['date'].dt.date
|
||||||
|
df['date'] = df['date'].astype(str)
|
||||||
|
|
||||||
|
df_on_date = df[df['date'] == date]
|
||||||
|
return df_on_date
|
||||||
|
|
||||||
|
|
||||||
|
def get_top_species(df, limit=10):
|
||||||
|
return df['common_name'].value_counts()[:CONFIG['readings']]
|
||||||
|
|
||||||
|
|
||||||
|
def get_top_detections(df, limit=10):
|
||||||
|
df_top_species = get_top_species(df, limit=limit)
|
||||||
|
return df[df['common_name'].isin(df_top_species.index)]
|
||||||
|
|
||||||
|
|
||||||
|
def get_frequence_order(df, limit=10):
|
||||||
|
pd.value_counts(df['common_name']).iloc[:limit]
|
||||||
|
|
||||||
|
def presence_chart(date, filename):
|
||||||
|
df_detections = get_detection_hourly(date)
|
||||||
|
df_top_detections = get_top_detections(df_detections, limit=CONFIG['readings'])
|
||||||
|
fig, axs = plt.subplots(1, 2, figsize=(15, 4), gridspec_kw=dict(
|
||||||
|
width_ratios=[3, 6]))
|
||||||
|
plt.subplots_adjust(left=None, bottom=None, right=None,
|
||||||
|
top=None, wspace=0, hspace=0)
|
||||||
|
|
||||||
|
frequencies_order = get_frequence_order(df_detections, limit=CONFIG["readings"])
|
||||||
|
# Get min max confidences
|
||||||
|
confidence_minmax = df_detections.groupby('common_name')['confidence'].max()
|
||||||
|
# Norm values for color palette
|
||||||
|
norm = plt.Normalize(confidence_minmax.values.min(),
|
||||||
|
confidence_minmax.values.max())
|
||||||
|
colors = plt.cm.Greens(norm(confidence_minmax))
|
||||||
|
plot = sns.countplot(y='common_name', data=df_top_detections, palette=colors, order=frequencies_order, ax=axs[0])
|
||||||
|
|
||||||
|
plot.set(ylabel=None)
|
||||||
|
plot.set(xlabel="Detections")
|
||||||
|
|
||||||
|
heat = pd.crosstab(df_top_detections['common_name'], df_top_detections['hour'])
|
||||||
|
# Order heatmap Birds by frequency of occurrance
|
||||||
|
heat.index = pd.CategoricalIndex(heat.index, categories=frequencies_order)
|
||||||
|
heat.sort_index(level=0, inplace=True)
|
||||||
|
|
||||||
|
hours_in_day = pd.Series(data=range(0, 24))
|
||||||
|
heat_frame = pd.DataFrame(data=0, index=heat.index, columns=hours_in_day)
|
||||||
|
heat = (heat + heat_frame).fillna(0)
|
||||||
|
|
||||||
|
# Generate heatmap plot
|
||||||
|
plot = sns.heatmap(
|
||||||
|
heat,
|
||||||
|
norm=LogNorm(),
|
||||||
|
annot=True,
|
||||||
|
annot_kws={
|
||||||
|
"fontsize": 7
|
||||||
|
},
|
||||||
|
fmt="g",
|
||||||
|
cmap=CONFIG['palette'],
|
||||||
|
square=False,
|
||||||
|
cbar=False,
|
||||||
|
linewidth=0.5,
|
||||||
|
linecolor="Grey",
|
||||||
|
ax=axs[1],
|
||||||
|
yticklabels=False
|
||||||
|
)
|
||||||
|
plot.set_xticklabels(plot.get_xticklabels(), rotation=0, size=7)
|
||||||
|
|
||||||
|
for _, spine in plot.spines.items():
|
||||||
|
spine.set_visible(True)
|
||||||
|
|
||||||
|
plot.set(ylabel=None)
|
||||||
|
plot.set(xlabel="Hour of day")
|
||||||
|
fig.subplots_adjust(top=0.9)
|
||||||
|
plt.suptitle(f"Top {CONFIG['readings']} species (Updated on {datetime.now().strftime('%Y/%m-%d %H:%M')})")
|
||||||
|
|
||||||
|
plt.savefig(filename)
|
||||||
|
plt.close()
|
||||||
|
|
||||||
|
def main():
|
||||||
|
date = datetime.now().strftime('%Y%m%d')
|
||||||
|
presence_chart(date, f'./var/charts/chart_{date}.png')
|
||||||
|
# print(get_top_detections(get_detection_hourly(date), limit=10))
|
||||||
|
if not db is None:
|
||||||
|
db.close()
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
@ -1,11 +1,9 @@
|
|||||||
# Launch BirdNET-Analyzer on the previously recorded audio chunks
|
|
||||||
|
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=BirdNET-stream Analyzis
|
Description=BirdNET-stream Analyzis
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=<USER>
|
User=<USER>
|
||||||
Group=<USER>
|
Group=<GROUP>
|
||||||
WorkingDirectory=<DIR>
|
WorkingDirectory=<DIR>
|
||||||
ExecStart=bash ./daemon/birdnet_analyzis.sh
|
ExecStart=bash ./daemon/birdnet_analyzis.sh
|
||||||
Restart=always
|
Restart=always
|
||||||
|
@ -2,9 +2,9 @@
|
|||||||
Description=BirdNET-stream miner service
|
Description=BirdNET-stream miner service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=simple
|
||||||
User=<USER>
|
User=<USER>
|
||||||
GROUP=<GROUP>
|
Group=<GROUP>
|
||||||
WorkingDirectory=<DIR>
|
WorkingDirectory=<DIR>
|
||||||
ExecStart=bash ./daemon/birdnet_miner.sh
|
ExecStart=bash ./daemon/birdnet_miner.sh
|
||||||
RemainAfterExit=yes
|
RemainAfterExit=yes
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
Description=BirdNET-stream miner Timer
|
Description=BirdNET-stream miner Timer
|
||||||
|
|
||||||
[Timer]
|
[Timer]
|
||||||
OnCalendar=*-*-* *:00
|
OnCalendar=*:0/15
|
||||||
Unit=birdnet_miner.service
|
Unit=birdnet_miner.service
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
12
daemon/systemd/templates/birdnet_plotter.service
Normal file
12
daemon/systemd/templates/birdnet_plotter.service
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=BirdNET-stream plotter
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
User=<USER>
|
||||||
|
Group=<GROUP>
|
||||||
|
WorkingDirectory=<DIR>
|
||||||
|
ExecStart=./.venv/birdnet-stream/bin/python3 ./daemon/plotter/chart.py
|
||||||
|
Type=simple
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
9
daemon/systemd/templates/birdnet_plotter.timer
Normal file
9
daemon/systemd/templates/birdnet_plotter.timer
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=BirdNET-stream plotter timer
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar=*:00
|
||||||
|
Unit=birdnet_plotter.service
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=basic.target
|
@ -5,7 +5,7 @@ Description=BirdNET-stream recording
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
User=<USER>
|
User=<USER>
|
||||||
Group=<DIR>
|
Group=<GROUP>
|
||||||
WorkingDirectory=<DIR>
|
WorkingDirectory=<DIR>
|
||||||
ExecStart=bash ./daemon/birdnet_recording.sh
|
ExecStart=bash ./daemon/birdnet_recording.sh
|
||||||
Restart=always
|
Restart=always
|
||||||
|
@ -64,7 +64,7 @@ install_birdnetstream_services() {
|
|||||||
DIR=$(pwd)
|
DIR=$(pwd)
|
||||||
GROUP=$USER
|
GROUP=$USER
|
||||||
debug "Setting up BirdNET stream systemd services"
|
debug "Setting up BirdNET stream systemd services"
|
||||||
services="birdnet_recording.service birdnet_analyzis.service birdnet_miner.timer birdnet_miner.service"
|
services="birdnet_recording.service birdnet_analyzis.service birdnet_miner.timer birdnet_miner.service birdnet_plotter.service birdnet_plotter.timer"
|
||||||
read -r -a services_array <<<"$services"
|
read -r -a services_array <<<"$services"
|
||||||
|
|
||||||
for service in ${services_array[@]}; do
|
for service in ${services_array[@]}; do
|
||||||
@ -75,7 +75,7 @@ install_birdnetstream_services() {
|
|||||||
done
|
done
|
||||||
done
|
done
|
||||||
sudo systemctl daemon-reload
|
sudo systemctl daemon-reload
|
||||||
sudo systemctl enable --now birdnet_recording.service birdnet_analyzis.service birdnet_miner.timer
|
sudo systemctl enable --now birdnet_recording.service birdnet_analyzis.service birdnet_miner.timer birdnet_plotter.timer
|
||||||
}
|
}
|
||||||
|
|
||||||
install_php8() {
|
install_php8() {
|
||||||
|
@ -3,22 +3,33 @@ audioread==2.1.9
|
|||||||
certifi==2022.6.15
|
certifi==2022.6.15
|
||||||
cffi==1.15.1
|
cffi==1.15.1
|
||||||
charset-normalizer==2.1.0
|
charset-normalizer==2.1.0
|
||||||
|
cycler==0.11.0
|
||||||
decorator==5.1.1
|
decorator==5.1.1
|
||||||
|
fonttools==4.34.4
|
||||||
idna==3.3
|
idna==3.3
|
||||||
joblib==1.1.0
|
joblib==1.1.0
|
||||||
|
kiwisolver==1.4.4
|
||||||
librosa==0.9.2
|
librosa==0.9.2
|
||||||
llvmlite==0.39.0
|
llvmlite==0.39.0
|
||||||
|
matplotlib==3.5.3
|
||||||
numba==0.56.0
|
numba==0.56.0
|
||||||
numpy==1.22.4
|
numpy==1.22.4
|
||||||
packaging==21.3
|
packaging==21.3
|
||||||
|
pandas==1.4.3
|
||||||
|
Pillow==9.2.0
|
||||||
pooch==1.6.0
|
pooch==1.6.0
|
||||||
pycparser==2.21
|
pycparser==2.21
|
||||||
pyparsing==3.0.9
|
pyparsing==3.0.9
|
||||||
|
python-dateutil==2.8.2
|
||||||
|
pytz==2022.2.1
|
||||||
requests==2.28.1
|
requests==2.28.1
|
||||||
resampy==0.3.1
|
resampy==0.3.1
|
||||||
scikit-learn==1.1.2
|
scikit-learn==1.1.2
|
||||||
scipy==1.9.0
|
scipy==1.9.0
|
||||||
|
seaborn==0.11.2
|
||||||
|
six==1.16.0
|
||||||
SoundFile==0.10.3.post1
|
SoundFile==0.10.3.post1
|
||||||
tflite-runtime==2.9.1
|
tflite-runtime==2.9.1
|
||||||
threadpoolctl==3.1.0
|
threadpoolctl==3.1.0
|
||||||
urllib3==1.26.11
|
urllib3==1.26.11
|
||||||
|
xmpppy==0.7.1
|
||||||
|
@ -58,7 +58,6 @@ header img.logo {
|
|||||||
main {
|
main {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 5em;
|
padding: 5em;
|
||||||
z-index: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
@ -117,7 +116,7 @@ canvas {
|
|||||||
#statuses .grid {
|
#statuses .grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 1fr 1fr 1fr;
|
grid-template-rows: 1fr 1fr 1fr;
|
||||||
grid-template-columns: 1fr 1fr 1fr;
|
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||||
|
|
||||||
gap: 0.1em;
|
gap: 0.1em;
|
||||||
}
|
}
|
||||||
@ -130,13 +129,18 @@ canvas {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 0.4em;
|
top: 0.4em;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.inactive.bullet {
|
||||||
|
background-color: #999;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.active.bullet {
|
.status.active.bullet {
|
||||||
background-color: #090;
|
background-color: #090;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status.inactive.bullet {
|
.status.dead.bullet {
|
||||||
background-color: #900;
|
background-color: #900;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -152,3 +156,12 @@ canvas {
|
|||||||
background-color: rgba(255, 255, 255, 1);
|
background-color: rgba(255, 255, 255, 1);
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logs {
|
||||||
|
background-color: black;
|
||||||
|
color: white;
|
||||||
|
font: monospace;
|
||||||
|
padding: 1em;
|
||||||
|
overflow-y: scroll;
|
||||||
|
height: 100%;
|
||||||
|
}
|
@ -3,6 +3,7 @@ nav {
|
|||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
--nav-width: 20em;
|
--nav-width: 20em;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggler{
|
.toggler{
|
||||||
|
38
www/src/Controller/LogsController.php
Normal file
38
www/src/Controller/LogsController.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
|
||||||
|
class LogsController extends AbstractController
|
||||||
|
{
|
||||||
|
private $allowed_services = "recording analyzis miner plotter";
|
||||||
|
/**
|
||||||
|
* @Route("/logs/{service}", name="logs")
|
||||||
|
*/
|
||||||
|
public function logs($service = "all")
|
||||||
|
{
|
||||||
|
$logs = "";
|
||||||
|
if ($service === "all") {
|
||||||
|
foreach (explode(" ", $this->allowed_services) as $service) {
|
||||||
|
$logs .= $this->journal_logs($service);
|
||||||
|
}
|
||||||
|
} else if (str_contains($this->allowed_services, $service)) {
|
||||||
|
$logs .= $this->journal_logs($service);
|
||||||
|
} else {
|
||||||
|
return new Response("Service not found", Response::HTTP_BAD_REQUEST);
|
||||||
|
}
|
||||||
|
return $this->render('logs/logs.html.twig', [
|
||||||
|
'logs' => $logs
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function journal_logs($service)
|
||||||
|
{
|
||||||
|
$logs = shell_exec("journalctl -u birdnet_recording -n 10");
|
||||||
|
return $logs;
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,6 @@ use Symfony\Component\HttpFoundation\Response;
|
|||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\Routing\Annotation\Route;
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
use Sensio\Bundle\FrameworkExtraBundle\EventListener\ControllerListener;
|
|
||||||
|
|
||||||
class ServicesController extends AbstractController
|
class ServicesController extends AbstractController
|
||||||
{
|
{
|
||||||
@ -15,7 +14,7 @@ class ServicesController extends AbstractController
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route("/service/status", name="service_status")
|
* @Route("/services/status", name="service_status")
|
||||||
*/
|
*/
|
||||||
public function service_status() {
|
public function service_status() {
|
||||||
$status = array_map(function($service) {
|
$status = array_map(function($service) {
|
||||||
@ -30,15 +29,22 @@ class ServicesController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @Route("/service/manage/{action}", name="service_manager")
|
* @Route("/services/manage/{action}/{service}", name="service_manager")
|
||||||
*/
|
*/
|
||||||
public function service_manage($action, $service)
|
public function service_manage($action, $service="all")
|
||||||
{
|
{
|
||||||
$error = "";
|
$error = "";
|
||||||
if (in_array($action, $this->allowed_actions)) {
|
if (in_array($action, $this->allowed_actions)) {
|
||||||
if (in_array($service, $this->services_available)) {
|
if ($service == "all") {
|
||||||
|
foreach ($this->services_available as $service) {
|
||||||
if(($output = $this->manage_systemd_service($action, $service)) != "true") {
|
if(($output = $this->manage_systemd_service($action, $service)) != "true") {
|
||||||
$error = "Error while managing service";
|
$error .= "Error while managing $service service";
|
||||||
|
dump($output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (in_array($service, $this->services_available)) {
|
||||||
|
if(($output = $this->manage_systemd_service($action, $service)) != "true") {
|
||||||
|
$error .= "Error while managing $service service";
|
||||||
dump($output);
|
dump($output);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -56,18 +62,46 @@ class ServicesController extends AbstractController
|
|||||||
|
|
||||||
private function manage_systemd_service($action, $service)
|
private function manage_systemd_service($action, $service)
|
||||||
{
|
{
|
||||||
$command = "./daemon/birdnet_manager.sh ".$action;
|
// TODO correct this command (failed with not root user)
|
||||||
$workdir = $this->getParameter("kernel.project_dir") . "/../";
|
$command = "./daemon/birdnet_manager.sh $action birdnet_$service";
|
||||||
$command = "cd ".$workdir." && ".$command;
|
$old_path = getcwd();
|
||||||
echo $command;
|
$workdir = $this->getParameter("kernel.project_dir");
|
||||||
|
chdir($workdir);
|
||||||
$output = shell_exec($command);
|
$output = shell_exec($command);
|
||||||
|
dump($output);
|
||||||
|
chdir($old_path);
|
||||||
return $output;
|
return $output;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function systemd_service_status($service)
|
private function systemd_service_status($service)
|
||||||
{
|
{
|
||||||
|
$status = array();
|
||||||
$command = "systemctl is-active birdnet_".$service.".service";
|
$command = "systemctl is-active birdnet_".$service.".service";
|
||||||
$result = shell_exec($command);
|
$output = shell_exec($command);
|
||||||
return $result;
|
if (! is_null($output))
|
||||||
|
$status["status"] = $output;
|
||||||
|
else
|
||||||
|
$status["status"] = "unknown";
|
||||||
|
$command = "systemctl is-enabled birdnet_".$service.".service";
|
||||||
|
$output = shell_exec($command);
|
||||||
|
if (! is_null($output))
|
||||||
|
$status["enabled"] = $output;
|
||||||
|
else
|
||||||
|
$status["enabled"] = "unknown";
|
||||||
|
$status["eta"] = $this->systemd_timer_eta($service);
|
||||||
|
return $status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function systemd_timer_eta($service)
|
||||||
|
{
|
||||||
|
$eta = "";
|
||||||
|
$command = "systemctl list-timers | grep $service.timer | cut -d' ' -f5";
|
||||||
|
$output = shell_exec($command);
|
||||||
|
// dump($output);
|
||||||
|
if (! is_null($output))
|
||||||
|
$eta = $output;
|
||||||
|
else
|
||||||
|
$eta = "na";
|
||||||
|
return $eta;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,7 +1,7 @@
|
|||||||
{% extends "base.html.twig" %}
|
{% extends "base.html.twig" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<p>Welcome to BirdNET-stream !</p>
|
<p>{{ "Welcome to BirdNET-stream !" | trans }}</p>
|
||||||
{% include "stats.html.twig" %}
|
{% include "stats.html.twig" %}
|
||||||
{% include "chart.html.twig" %}
|
{% include "chart.html.twig" %}
|
||||||
{% endblock %}
|
{% endblock %}
|
14
www/templates/logs/logs.html.twig
Normal file
14
www/templates/logs/logs.html.twig
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{% extends "base.html.twig" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h2>
|
||||||
|
{{ "Logs" | trans }}
|
||||||
|
</h2>
|
||||||
|
{% if logs is defined and logs | length > 0 %}
|
||||||
|
<pre class="logs">
|
||||||
|
{{ logs }}
|
||||||
|
</pre>
|
||||||
|
{% else %}
|
||||||
|
<p>{{ "No logs available" | trans }}</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
@ -7,43 +7,45 @@
|
|||||||
<div class="menu overlay">
|
<div class="menu overlay">
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
<a href="/">Home</a>
|
<a href="/">{{ "Home" | trans }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/about">About</a>
|
<a href="/about">{{ "About" | trans }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/today">Today's Detections</a>
|
<a href="/today">{{ "Today's Detections" | trans }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/spectro">Spectrogram</a>
|
<a href="/spectro">{{ "Spectrogram" | trans }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/stats">Species Stats</a>
|
<a href="/stats">{{ "Species Stats" | trans }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="dropdown">
|
<li class="dropdown">
|
||||||
<a href="/records" class="dropdown-button">Recordings</a>
|
<a href="/records" class="dropdown-button">{{ "Recordings" | trans }}</a>
|
||||||
<ul class="dropdown-content">
|
<ul class="dropdown-content">
|
||||||
<li>
|
<li>
|
||||||
<a href="/records/bests">
|
<a href="/records/bests">
|
||||||
Best Recordings
|
{{ "Best Recordings" | trans }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/charts">Daily Charts</a>
|
<a href="/charts">{{ "Daily Charts" | trans }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/logs">View Logs</a>
|
<a href="/logs">{{ "View Logs" | trans }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/services/status">{{ "Status" | trans }}</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="/tools">
|
<a href="/tools">
|
||||||
Tools
|
{{ "Tools" | trans }}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% include "title.html.twig" %}
|
{% include "title.html.twig" %}
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -8,9 +8,11 @@
|
|||||||
<ul id="statuses" class="container column">
|
<ul id="statuses" class="container column">
|
||||||
{% for service in status %}
|
{% for service in status %}
|
||||||
<li class="grid">
|
<li class="grid">
|
||||||
<div class="col status bullet {{ service["status"] }}"></div>
|
<div class="col status bullet {{ service.status.status }}"></div>
|
||||||
<div class="col name">{{ service["name"] }}</div>
|
<div class="col name">{{ service.name }}</div>
|
||||||
<div class="col status">{{ service["status"] }}</div>
|
<div class="col status">{{ service.status.status }}</div>
|
||||||
|
<div class="col status">{{ service.status.enabled }}</div>
|
||||||
|
<div class="col eta">{{ service.status.eta }}</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<div id="stats">
|
<div id="stats">
|
||||||
<h2>Quick Stats</h2>
|
<h2>{{ "Quick Stats" | trans }}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<li class="most-recorded-species">
|
<li class="most-recorded-species">
|
||||||
{{ "Most recorded species" | trans }}:
|
{{ "Most recorded species" | trans }}:
|
||||||
|
Loading…
Reference in New Issue
Block a user