Add plotter and fix miner
This commit is contained in:
parent
d56f3806fa
commit
acc51fdfc4
|
@ -8,4 +8,4 @@ species_list.txt
|
|||
|
||||
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()
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
<?php
|
||||
echo shell_exec("journalctl -u birdnet_recording -n 10");
|
|
@ -0,0 +1,3 @@
|
|||
<?php
|
||||
|
||||
echo urlencode("contact@ortion.fr");
|
3
TODO
3
TODO
|
@ -1 +1,4 @@
|
|||
- 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
|
||||
# Recording duration (in seconds)
|
||||
RECORDING_DURATION=15
|
||||
RECORDING_AMPLIFICATION=1.5
|
||||
# Chunk folder location
|
||||
CHUNK_FOLDER="./var/chunks"
|
||||
# Audio recording device (pulseaudio)
|
||||
|
@ -18,3 +19,10 @@ AUDIO_DEVICE="default"
|
|||
PYTHON_VENV="./.venv/birdnet-stream"
|
||||
# Database location
|
||||
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
|
||||
}
|
||||
|
||||
check_prerequisites
|
||||
|
||||
# Get list of current chunk in working directory
|
||||
chunks=$(get_chunk_list)
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#! /usr/bin/env bash
|
||||
|
||||
# inspired by https://unix.stackexchange.com/questions/47132/execute-shell-script-from-php-as-root-user
|
||||
set -e
|
||||
# set -x
|
||||
|
||||
|
@ -9,19 +9,19 @@ if [ -f "$config_filepath" ]; then
|
|||
source "$config_filepath"
|
||||
else
|
||||
echo "Config file not found: $config_filepath"
|
||||
exit 1
|
||||
# exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $DAEMON_USER ]]
|
||||
then
|
||||
echo "DAEMON_USER is not set"
|
||||
exit 1
|
||||
# exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $DAEMON_PASSWORD ]]
|
||||
then
|
||||
echo "DAEMON_PASSWORD is not set"
|
||||
exit 1
|
||||
# exit 1
|
||||
fi
|
||||
|
||||
SERVICES="$(sudo -S <<< $DAEMON_PASSWORD ls /etc/systemd/system/ | grep 'birdnet')"
|
||||
|
@ -36,17 +36,15 @@ debug() {
|
|||
|
||||
manage() {
|
||||
action=$1
|
||||
if [[ -z $2 ]]; then
|
||||
services=$SERVICES
|
||||
else
|
||||
services=$2
|
||||
fi
|
||||
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"
|
||||
}
|
||||
|
||||
stop() {
|
||||
manage stop
|
||||
}
|
||||
|
||||
start() {
|
||||
manage start
|
||||
}
|
||||
|
||||
manage $1
|
||||
manage $1 $2
|
|
@ -3,7 +3,6 @@
|
|||
#
|
||||
|
||||
DEBUG=${DEBUG:-1}
|
||||
|
||||
set -e
|
||||
# set -x
|
||||
|
||||
|
@ -99,9 +98,8 @@ save_observations() {
|
|||
location_id=$(get_location_id "$LATITUDE" "$LONGITUDE")
|
||||
fi
|
||||
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"
|
||||
exit 1
|
||||
else
|
||||
debug "Inserting observation: $source_audio, $start, $end, $taxon_id, $location_id, $datetime"
|
||||
insert_observation "$source_audio" "$start" "$end" "$taxon_id" "$location_id" "$confidence" "$datetime"
|
||||
|
|
|
@ -32,7 +32,7 @@ record() {
|
|||
DEVICE=$1
|
||||
DURATION=$2
|
||||
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"
|
||||
|
|
|
@ -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,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]
|
||||
Description=BirdNET-stream Analyzis
|
||||
|
||||
[Service]
|
||||
User=<USER>
|
||||
Group=<USER>
|
||||
Group=<GROUP>
|
||||
WorkingDirectory=<DIR>
|
||||
ExecStart=bash ./daemon/birdnet_analyzis.sh
|
||||
Restart=always
|
||||
|
|
|
@ -2,9 +2,9 @@
|
|||
Description=BirdNET-stream miner service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
Type=simple
|
||||
User=<USER>
|
||||
GROUP=<GROUP>
|
||||
Group=<GROUP>
|
||||
WorkingDirectory=<DIR>
|
||||
ExecStart=bash ./daemon/birdnet_miner.sh
|
||||
RemainAfterExit=yes
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
Description=BirdNET-stream miner Timer
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* *:00
|
||||
OnCalendar=*:0/15
|
||||
Unit=birdnet_miner.service
|
||||
|
||||
[Install]
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
User=<USER>
|
||||
Group=<DIR>
|
||||
Group=<GROUP>
|
||||
WorkingDirectory=<DIR>
|
||||
ExecStart=bash ./daemon/birdnet_recording.sh
|
||||
Restart=always
|
||||
|
|
|
@ -64,7 +64,7 @@ install_birdnetstream_services() {
|
|||
DIR=$(pwd)
|
||||
GROUP=$USER
|
||||
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"
|
||||
|
||||
for service in ${services_array[@]}; do
|
||||
|
@ -75,7 +75,7 @@ install_birdnetstream_services() {
|
|||
done
|
||||
done
|
||||
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() {
|
||||
|
|
|
@ -3,22 +3,33 @@ audioread==2.1.9
|
|||
certifi==2022.6.15
|
||||
cffi==1.15.1
|
||||
charset-normalizer==2.1.0
|
||||
cycler==0.11.0
|
||||
decorator==5.1.1
|
||||
fonttools==4.34.4
|
||||
idna==3.3
|
||||
joblib==1.1.0
|
||||
kiwisolver==1.4.4
|
||||
librosa==0.9.2
|
||||
llvmlite==0.39.0
|
||||
matplotlib==3.5.3
|
||||
numba==0.56.0
|
||||
numpy==1.22.4
|
||||
packaging==21.3
|
||||
pandas==1.4.3
|
||||
Pillow==9.2.0
|
||||
pooch==1.6.0
|
||||
pycparser==2.21
|
||||
pyparsing==3.0.9
|
||||
python-dateutil==2.8.2
|
||||
pytz==2022.2.1
|
||||
requests==2.28.1
|
||||
resampy==0.3.1
|
||||
scikit-learn==1.1.2
|
||||
scipy==1.9.0
|
||||
seaborn==0.11.2
|
||||
six==1.16.0
|
||||
SoundFile==0.10.3.post1
|
||||
tflite-runtime==2.9.1
|
||||
threadpoolctl==3.1.0
|
||||
urllib3==1.26.11
|
||||
xmpppy==0.7.1
|
||||
|
|
|
@ -58,7 +58,6 @@ header img.logo {
|
|||
main {
|
||||
min-height: 100vh;
|
||||
padding: 5em;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
|
@ -117,7 +116,7 @@ canvas {
|
|||
#statuses .grid {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
|
||||
|
||||
gap: 0.1em;
|
||||
}
|
||||
|
@ -130,13 +129,18 @@ canvas {
|
|||
display: inline-block;
|
||||
position: relative;
|
||||
top: 0.4em;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.status.inactive.bullet {
|
||||
background-color: #999;
|
||||
}
|
||||
|
||||
.status.active.bullet {
|
||||
background-color: #090;
|
||||
}
|
||||
|
||||
.status.inactive.bullet {
|
||||
.status.dead.bullet {
|
||||
background-color: #900;
|
||||
}
|
||||
|
||||
|
@ -151,4 +155,13 @@ canvas {
|
|||
opacity: 100%;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.logs {
|
||||
background-color: black;
|
||||
color: white;
|
||||
font: monospace;
|
||||
padding: 1em;
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
|
@ -3,6 +3,7 @@ nav {
|
|||
top: 0;
|
||||
left: 0;
|
||||
--nav-width: 20em;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.toggler{
|
||||
|
|
|
@ -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\Routing\Annotation\Route;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\EventListener\ControllerListener;
|
||||
|
||||
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() {
|
||||
$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 = "";
|
||||
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") {
|
||||
$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";
|
||||
$error .= "Error while managing $service service";
|
||||
dump($output);
|
||||
}
|
||||
} else {
|
||||
|
@ -56,18 +62,46 @@ class ServicesController extends AbstractController
|
|||
|
||||
private function manage_systemd_service($action, $service)
|
||||
{
|
||||
$command = "./daemon/birdnet_manager.sh ".$action;
|
||||
$workdir = $this->getParameter("kernel.project_dir") . "/../";
|
||||
$command = "cd ".$workdir." && ".$command;
|
||||
echo $command;
|
||||
// TODO correct this command (failed with not root user)
|
||||
$command = "./daemon/birdnet_manager.sh $action birdnet_$service";
|
||||
$old_path = getcwd();
|
||||
$workdir = $this->getParameter("kernel.project_dir");
|
||||
chdir($workdir);
|
||||
$output = shell_exec($command);
|
||||
dump($output);
|
||||
chdir($old_path);
|
||||
return $output;
|
||||
}
|
||||
|
||||
private function systemd_service_status($service)
|
||||
{
|
||||
$command = "systemctl is-active birdnet_".$service.".service";
|
||||
$result = shell_exec($command);
|
||||
return $result;
|
||||
$status = array();
|
||||
$command = "systemctl is-active birdnet_".$service.".service";
|
||||
$output = shell_exec($command);
|
||||
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" %}
|
||||
|
||||
{% block content %}
|
||||
<p>Welcome to BirdNET-stream !</p>
|
||||
<p>{{ "Welcome to BirdNET-stream !" | trans }}</p>
|
||||
{% include "stats.html.twig" %}
|
||||
{% include "chart.html.twig" %}
|
||||
{% endblock %}
|
|
@ -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 %}
|
|
@ -1,49 +1,51 @@
|
|||
<nav class="navbar">
|
||||
<input type="checkbox" class="toggler">
|
||||
<div class="hamburger">
|
||||
<div class="hamburger">
|
||||
<div></div>
|
||||
</div>
|
||||
<div class="fill"></div>
|
||||
<div class="fill"></div>
|
||||
<div class="menu overlay">
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/">Home</a>
|
||||
<a href="/">{{ "Home" | trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about">About</a>
|
||||
<a href="/about">{{ "About" | trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/today">Today's Detections</a>
|
||||
<a href="/today">{{ "Today's Detections" | trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/spectro">Spectrogram</a>
|
||||
<a href="/spectro">{{ "Spectrogram" | trans }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/stats">Species Stats</a>
|
||||
<a href="/stats">{{ "Species Stats" | trans }}</a>
|
||||
</li>
|
||||
<li class="dropdown">
|
||||
<a href="/records" class="dropdown-button">Recordings</a>
|
||||
<a href="/records" class="dropdown-button">{{ "Recordings" | trans }}</a>
|
||||
<ul class="dropdown-content">
|
||||
<li>
|
||||
<a href="/records/bests">
|
||||
Best Recordings
|
||||
{{ "Best Recordings" | trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/charts">Daily Charts</a>
|
||||
<a href="/charts">{{ "Daily Charts" | trans }}</a>
|
||||
</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>
|
||||
<a href="/tools">
|
||||
Tools
|
||||
{{ "Tools" | trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
{% include "title.html.twig" %}
|
||||
</nav>
|
||||
|
|
|
@ -8,9 +8,11 @@
|
|||
<ul id="statuses" class="container column">
|
||||
{% for service in status %}
|
||||
<li class="grid">
|
||||
<div class="col status bullet {{ service["status"] }}"></div>
|
||||
<div class="col name">{{ service["name"] }}</div>
|
||||
<div class="col status">{{ service["status"] }}</div>
|
||||
<div class="col status bullet {{ service.status.status }}"></div>
|
||||
<div class="col name">{{ service.name }}</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>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<div id="stats">
|
||||
<h2>Quick Stats</h2>
|
||||
<h2>{{ "Quick Stats" | trans }}</h2>
|
||||
<ul>
|
||||
<li class="most-recorded-species">
|
||||
{{ "Most recorded species" | trans }}:
|
||||
|
|
Loading…
Reference in New Issue