Add plotter and fix miner

This commit is contained in:
Samuel Ortion 2022-08-16 05:21:53 +02:00
parent d56f3806fa
commit acc51fdfc4
31 changed files with 343 additions and 286 deletions

2
.gitignore vendored
View File

@ -8,4 +8,4 @@ species_list.txt
push.sh push.sh
config/analyzer.conf config/*.conf

View File

@ -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
View File

@ -0,0 +1,2 @@
<?php
echo shell_exec("journalctl -u birdnet_recording -n 10");

3
.ideas/url_escape.php Normal file
View File

@ -0,0 +1,3 @@
<?php
echo urlencode("contact@ortion.fr");

3
TODO
View File

@ -1 +1,4 @@
- Fix install of venv - Fix install of venv
- Fix clean script
- Change install script for php 8.1
- Fix service manager

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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"

View File

@ -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
View 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

View File

120
daemon/plotter/chart.py Executable file
View 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()

View File

@ -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

View File

@ -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

View File

@ -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]

View 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

View File

@ -0,0 +1,9 @@
[Unit]
Description=BirdNET-stream plotter timer
[Timer]
OnCalendar=*:00
Unit=birdnet_plotter.service
[Install]
WantedBy=basic.target

View File

@ -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

View File

@ -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() {

View File

@ -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

View File

@ -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%;
}

View File

@ -3,6 +3,7 @@ nav {
top: 0; top: 0;
left: 0; left: 0;
--nav-width: 20em; --nav-width: 20em;
z-index: 10;
} }
.toggler{ .toggler{

View 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;
}
}

View File

@ -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;
} }
} }

View File

@ -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 %}

View 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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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 }}: