Introduzione
Videosorveglianza con Raspberry Pi e Docker: tutorial completo passo dopo passo. Se stai cercando una guida per creare un sistema di videosorveglianza fai-da-te, sei nel posto giusto. Questo tutorial completo ti guiderà passo dopo passo nella configurazione di un sistema basato su Raspberry Pi, una soluzione economica e versatile, perfetta per progetti di domotica e sicurezza. Utilizzeremo tecnologie moderne come Docker, Motion , FastAPI e React, per costruire un sistema scalabile, facile da gestire e accessibile sia da esperti che da principianti.
Il cuore del sistema è la Raspberry Pi, che con il supporto di Docker containerizza il backend e il frontend, rendendo l’installazione e la manutenzione estremamente semplici. Grazie a Motion, la fotocamera rileverà il movimento e catturerà immagini o video, archiviandoli localmente. Un’interfaccia utente costruita con React permetterà di visualizzare e gestire il materiale acquisito, con funzioni come la paginazione e l’anteprima delle miniature. Tutto questo è coordinato da un backend scritto in Python con FastAPI, che gestisce le API REST per l’intero sistema, incluse notifiche tramite Telegram, Gmail e Pushover.
Inoltre, se desideri accedere al sistema di videosorveglianza anche quando sei lontano da casa, è possibile configurare una connessione VPN sicura con WireGuard. Questo ti permetterà di collegarti alla Raspberry Pi come se fossi nella stessa rete locale, garantendo un accesso remoto protetto senza esporre la tua rete su Internet. Tuttavia, questa configurazione richiede alcune condizioni tecniche specifiche, come il possesso di un IP pubblico accessibile e la possibilità di aprire porte sul router. Nel tutorial affronterò questi aspetti e fornirò una guida passo dopo passo per verificare se il tuo ambiente è compatibile con questa soluzione.
Che tu voglia monitorare la tua casa, il tuo ufficio o un piccolo laboratorio, questo sistema rappresenta una soluzione completa e altamente personalizzabile. Con pochi passi, potrai avere un sistema di videosorveglianza robusto e flessibile, che sfrutta appieno le potenzialità del Raspberry Pi.
👉 Se cerchi soluzioni alternative, dai un’occhiata anche ai nostri progetti di videosorveglianza con ESP32, tra cui una versione compatta con Telegram e una telecamera WiFi motorizzata controllabile via web.
Cosa può fare il nostro sistema di videosorveglianza
Il sistema di videosorveglianza che ti guideremo a realizzare offre una soluzione pratica e completa per monitorare qualsiasi ambiente, sia esso la tua casa, l’ufficio o un piccolo laboratorio. Utilizzando una Raspberry Pi come cuore tecnologico, questo sistema combina versatilità e semplicità per soddisfare diverse esigenze di sicurezza. Ecco un riepilogo delle principali funzionalità che avrai a disposizione:
- Cattura di foto e video: grazie all’integrazione con Motion, il sistema rileva automaticamente il movimento e registra foto o video in alta qualità, che vengono salvati localmente per garantirti il massimo controllo sui tuoi dati.
- Visualizzazione intuitiva: un’interfaccia grafica moderna ti permette di sfogliare facilmente foto e video, organizzati in una griglia con miniature. La funzione di paginazione ti consente di trovare rapidamente i file che ti servono, anche quando l’archivio è molto ampio.
- Notifiche in tempo reale: ricevi notifiche istantanee direttamente sul tuo smartphone o via email ogni volta che viene registrato un nuovo evento. Il sistema supporta Telegram, Gmail e Pushover, dandoti la libertà di scegliere il metodo di notifica che preferisci.
- Gestione completa degli utenti: con il ruolo di amministratore, puoi creare, modificare e rimuovere account per altri utenti, garantendo un accesso sicuro e controllato al sistema. Gli utenti normali possono accedere facilmente e personalizzare la propria password per una maggiore sicurezza.
- Configurazioni personalizzabili: tutto, dalle notifiche alla modalità di registrazione (foto o video), può essere personalizzato direttamente dall’interfaccia web, senza bisogno di intervenire sui file di configurazione manualmente.
- Facilità di installazione e manutenzione: grazie a Docker, l’intero sistema è containerizzato, rendendo l’installazione e l’aggiornamento semplicissimi, anche per chi non è esperto di tecnologia.
- Installazione semplice e automatizzata: l’installazione del sistema è praticamente automatica. Grazie agli automatismi offerti da Docker, ti basterà lanciare solo due script per configurare e avviare il sistema completo, senza doverti preoccupare di configurazioni complesse.
Con tutte queste funzionalità, questo sistema rappresenta una soluzione completa e flessibile per la videosorveglianza. Seguendo questo tutorial, potrai creare il tuo sistema passo dopo passo, adattandolo alle tue esigenze specifiche.
Cosa imparerai seguendo questo tutorial
Questo articolo non si limita a guidarti passo dopo passo nella realizzazione di un sistema di videosorveglianza: il suo vero obiettivo è farti scoprire e sperimentare una serie di tecnologie e concetti fondamentali nel mondo dell’informatica e della domotica. Ecco cosa imparerai seguendo questo progetto:
- Docker e la containerizzazione: capirai come Docker semplifica la gestione delle applicazioni, permettendo di creare un sistema modulare e facilmente distribuibile. Imparerai a usare i container per separare il backend e il frontend e a sfruttare Docker Compose per orchestrare il tutto con un solo comando.
- Gestione di API REST: esplorerai come funziona un backend moderno grazie a FastAPI, scoprendo come le API REST consentano al frontend di comunicare con il backend per gestire foto, video e configurazioni.
- Sviluppo di interfacce web: grazie all’utilizzo di React, apprenderai come progettare e costruire un’interfaccia utente dinamica, con funzionalità come la visualizzazione di miniature, la paginazione e la gestione delle configurazioni.
- Notifiche personalizzate: configurerai e utilizzerai sistemi di notifica come Telegram, Gmail e Pushover per ricevere aggiornamenti in tempo reale, integrandoli con il tuo sistema di sicurezza.
- Elaborazione immagini e video: scoprirai come generare miniature per foto e video, utilizzando librerie come Pillow e strumenti come FFmpeg per estrarre frame dai video.
- Gestione di utenti e sicurezza: imparerai a implementare un sistema di autenticazione con token JWT, a differenziare i ruoli degli utenti e a garantire che l’accesso sia sicuro e personalizzato.
- Configurazioni dinamiche: capirai come rendere un sistema facilmente configurabile, utilizzando file di configurazione e API per cambiare parametri senza dover accedere manualmente ai file del server.
- Debugging e sperimentazione: questo progetto ti incoraggerà a fare modifiche e provare nuove idee. Potrai sperimentare aggiungendo funzionalità, migliorando l’interfaccia o integrando nuove tecnologie, mettendo alla prova le competenze acquisite.
Che tu sia un appassionato di tecnologia o un curioso alle prime armi, questo tutorial ti permetterà di acquisire competenze pratiche che potrai riutilizzare in tanti altri progetti. La tua creatività è l’unico limite!
Backend e frontend: le due anime del sistema
Un sistema informatico moderno, come quello che stiamo costruendo per la videosorveglianza, è composto da due componenti principali: il backend (BE) e il frontend (FE). Questi due elementi lavorano insieme per offrire funzionalità robuste e un’esperienza utente intuitiva.
Cos’è il backend (BE)?
Il backend è la parte “dietro le quinte” del sistema. È il motore che gestisce i dati, le logiche di business e le interazioni con l’hardware, in questo caso la Raspberry Pi. Nel nostro progetto, il backend è responsabile di:
- Salvare e organizzare foto e video.
- Gestire configurazioni e notifiche.
- Garantire la sicurezza con autenticazione e autorizzazione degli utenti.
- Comunicare con il frontend tramite API REST.
Cos’è il frontend (FE)?
Il frontend è la parte visibile agli utenti: l’interfaccia grafica con cui interagiscono per utilizzare il sistema. È progettato per essere semplice e intuitivo, mostrando foto e video, e consentendo di accedere a tutte le funzioni offerte dal backend. Nel nostro progetto, il frontend:
- Mostra le miniature di foto e video in una griglia organizzata.
- Permette di sfogliare contenuti con paginazione.
- Consente di configurare il sistema e gestire gli utenti.
- Offre un’esperienza fluida sia su desktop che su mobile.
In sintesi, il backend è il cervello che elabora e gestisce i dati, mentre il frontend è la faccia del sistema, quella che l’utente vede e utilizza. La combinazione di un backend potente e un frontend intuitivo rende il nostro sistema di videosorveglianza completo e facile da usare.
FastAPI: il cuore del backend
Cos’è FastAPI?
FastAPI è un framework web moderno e veloce per creare API con Python. È progettato per essere facile da usare e altamente performante, sfruttando la potenza delle funzionalità asincrone di Python e il sistema di tipi statici di Python (type hints). Grazie alla sua flessibilità e semplicità, è ideale per progetti che richiedono API REST ben strutturate.
Dove lo usiamo nel progetto?
FastAPI rappresenta il cuore del backend del nostro sistema di videosorveglianza. Gestisce tutte le comunicazioni tra il frontend (React) e il Raspberry Pi. Le API REST che abbiamo implementato consentono di caricare e scaricare foto e video, generare miniature, inviare notifiche e persino aggiornare i file di configurazione del sistema.
Quali funzionalità svolge?
Nel nostro progetto, FastAPI assolve a diverse funzioni chiave:
- Gestione delle API REST: endpoint per il caricamento e il recupero di foto e video, configurazioni, notifiche, e gestione degli utenti.
- Database ORM: utilizzando SQLAlchemy, FastAPI archivia i metadati di foto e video, garantendo un accesso rapido e strutturato alle informazioni.
- Notifiche in tempo reale: invio di messaggi via Telegram, Pushover e Gmail quando nuovi file vengono caricati.
- Configurabilità del sistema: tramite endpoint dedicati, FastAPI consente di aggiornare i parametri di configurazione (come la modalità foto/video o i dettagli di notifica).
- Sicurezza: autenticazione e autorizzazione tramite token JWT, proteggendo l’accesso ai dati sensibili.
Grazie a FastAPI, il nostro sistema di videosorveglianza è robusto, scalabile e facile da mantenere, garantendo che ogni componente lavori in sinergia con gli altri.
React: l’Interfaccia intuitiva del frontend
Cos’è React?
React è una libreria JavaScript sviluppata da Facebook, utilizzata per creare interfacce utente interattive e componenti modulari. La sua filosofia si basa sul concetto di componenti riutilizzabili, che permettono di costruire interfacce scalabili e dinamiche.
Dove lo usiamo nel progetto?
React è il cuore del frontend del nostro sistema di videosorveglianza. Gestisce la visualizzazione e l’interazione dell’utente con il sistema, consentendo di accedere facilmente a foto, video e configurazioni tramite un’interfaccia user-friendly.
Quali funzionalità svolge?
Nel nostro progetto, React svolge diverse funzioni chiave:
- Visualizzazione di foto e video: con componenti come ThumbnailGrid e VideoGrid, React permette di visualizzare miniature, riprodurre video e accedere ai file archiviati.
- Paginazione: implementa un sistema avanzato di navigazione, che consente agli utenti di sfogliare foto e video in modo fluido e organizzato.
- Configurazioni e gestione utenti: tramite il componente AdminDashboard, React offre strumenti per l’amministrazione degli utenti e l’aggiornamento delle configurazioni di sistema.
- Feedback visivo: mostra notifiche o messaggi di errore direttamente all’utente, migliorando l’esperienza d’uso.
- Responsività: grazie a React, l’interfaccia si adatta perfettamente a diversi dispositivi, garantendo un’esperienza fluida sia su desktop che su dispositivi mobili.
React non è solo uno strumento per creare pagine belle da vedere, ma è anche il motore dietro l’interattività del sistema. Ogni azione dell’utente, come la navigazione tra le pagine o la visualizzazione di foto e video, è resa immediata ed efficace grazie alle sue capacità di rendering in tempo reale.
WireGuard: una VPN moderna, veloce e sicura per la videosorveglianza
WireGuard è un protocollo VPN (Virtual Private Network) moderno, progettato per offrire connessioni sicure, veloci e leggere rispetto alle soluzioni più tradizionali come OpenVPN e IPsec. Creato con un’architettura minimale e basata su crittografia di ultima generazione, WireGuard è oggi una delle soluzioni più affidabili per stabilire connessioni remote sicure.
A differenza di molte altre VPN, WireGuard è stato progettato con un’attenzione particolare alla semplicità e alle prestazioni. Il suo codice è estremamente compatto (meno di 4.000 righe), il che lo rende facile da analizzare per individuare eventuali vulnerabilità e garantire un livello di sicurezza elevato. Inoltre, il protocollo utilizza algoritmi di crittografia moderni e altamente ottimizzati, tra cui ChaCha20 per la cifratura dei dati e Curve25519 per l’autenticazione delle chiavi, garantendo sicurezza e velocità superiori rispetto alle soluzioni VPN più datate.
Perché WireGuard è la scelta ideale per la videosorveglianza su Raspberry Pi?
In un contesto di videosorveglianza, il bisogno di un accesso remoto sicuro è fondamentale. WireGuard si distingue per diversi vantaggi chiave:
- Semplicità di configurazione: a differenza di IPsec o OpenVPN, che richiedono configurazioni complesse e spesso difficili da debuggare, WireGuard utilizza file di configurazione chiari e facili da gestire.
- Prestazioni elevate: il protocollo è ottimizzato per funzionare con pochi consumi di risorse, rendendolo ideale per dispositivi con hardware limitato come Raspberry Pi.
- Sicurezza avanzata: l’uso di crittografia moderna assicura protezione contro attacchi di tipo MITM (man-in-the-middle) e altre vulnerabilità legate alle connessioni remote.
- Connessione stabile e veloce: grazie alla gestione efficiente delle chiavi di crittografia e alla ridotta latenza, WireGuard offre connessioni più fluide e affidabili rispetto ad altre VPN.
Come funziona WireGuard?
Il funzionamento di WireGuard si basa su un modello peer-to-peer, dove ogni dispositivo connesso alla VPN è identificato da una coppia di chiavi pubblica e privata. Il server stabilisce una connessione sicura con i client autorizzati, permettendo il traffico dati solo agli indirizzi IP specificati nella configurazione.
Nel caso della nostra applicazione di videosorveglianza, WireGuard consente di stabilire un tunnel privato tra la Raspberry Pi e i dispositivi remoti (smartphone, tablet o PC), in modo da poter accedere all’interfaccia web e ai flussi video in totale sicurezza, anche da reti pubbliche o non affidabili.
Un aspetto distintivo di WireGuard è che non mantiene connessioni persistenti: i pacchetti vengono trasmessi solo quando necessario, rendendo la VPN molto più efficiente dal punto di vista del consumo di banda e delle risorse di sistema.
WireGuard è sempre la soluzione giusta?
Sebbene WireGuard sia una tecnologia eccellente, ci sono alcuni limiti da considerare:
- Requisiti tecnici: per funzionare in accesso remoto, il server (la Raspberry Pi) deve avere un IP pubblico statico o la possibilità di utilizzare servizi come Dynamic DNS.
- Compatibilità con i provider internet: alcuni ISP potrebbero bloccare il traffico in entrata o limitare l’uso delle VPN, rendendo necessaria un’ulteriore configurazione avanzata.
- Possibili alternative: in casi di reti particolarmente restrittive, soluzioni come Tailscale (che si basa su WireGuard) possono offrire un’alternativa più semplice da implementare.
WireGuard rappresenta una soluzione eccellente per chi vuole un accesso remoto sicuro alla videosorveglianza senza compromettere le prestazioni. Grazie alla sua leggerezza, sicurezza e semplicità di configurazione, è perfetto per chi desidera controllare il proprio sistema in qualsiasi momento e da qualsiasi luogo, garantendo al contempo la massima protezione della rete domestica.
Di che componenti abbiamo bisogno per il progetto di videosorveglianza con Raspberry Pi e Docker?
La lista dei componenti non è particolarmente lunga:
- una camera da 5 Megapixel compatibile con Raspberry Pi 3 Model B compreso di cavo flessibile (flat)
- una (micro) SD card da 32GB/64GB formattata in FAT32
- un eventuale dongle WiFi USB per la Raspberry
- e, ovviamente, una Raspberry !
Ho testato SD card sia da 32GB che da 64GB.
Il progetto è stato testato con successo su una Raspberry PI 3 Model B. La Raspberry PI 3 Model B è da considerare come requisito minimo affinchè il sistema funzioni correttamente. Al momento attuale non è stabilito se il progetto possa essere utilizzato direttamente anche su modelli superiori di Raspberry.
Nella foto seguente puoi vedere la telecamera sia sul lato davanti che sul lato posteriore:

Mentre nella foto successiva puoi vedere un particolare della telecamera:

Preparazione della Raspberry
Per poter utilizzare la Raspberry è necessario fare alcuni passi preliminari ed installare alcuni software.
Iniziamo subito con l’installazione del sistema operativo.
Il sistema operativo scelto è una distribuzione fatta apposta per funzionare su tutti i tipi di Raspberry, anche le più datate. I test sono stati fatti su una Raspberry PI 3 Model B.
Se la Raspberry non ha connessione Wireless nativa puoi usare un dongle WiFi da inserire in una delle sue prese USB.
Scarichiamo e installiamo il sistema operativo su SD card
Scarica l’ultima versione del sistema operatico all’indirizzo https://www.raspberrypi.com/software/operating-systems/
e portati alla sezione Raspberry Pi OS (Legacy). Scaricherai una versione che non ha ambiente grafico in modo che sia la più leggera possibile:

Il file scaricato sarà compresso in formato xz. Per decomprimerlo su Linux dovrai prima installare il tool:
sudo dnf install xz su CentOS/RHEL/Fedora Linux.
sudo apt install xz-utils su Ubuntu/Debian
e poi dare la riga di comando:
xz -d -v filename.xz
dove filename.xz è il nome del file che hai appena scaricato contenente il sistema operativo.
Su Windows sarà sufficiente utilizzare uno fra questi tool: 7-Zip, winRAR, WinZip.
Il risultato sarà un file con estensione img che è l’immagine da flashare sulla SD card della Raspberry.
Per flashare l’immagine sulla SD card userai il tool Balena Etcher che funziona sia su Linux che su Windows che su MACOS.
Il suo utilizzo è molto semplice: è sufficiente selezionare l’immagine da flashare, la SD card di destinazione e premere il pulsante Flash.
Ecco come appare la sua interfaccia:

A sinistra viene impostata l’immagine da flashare, al centro la SD card da flashare, a destra il pulsante per iniziare l’operazione di flashing.
Alla fine della operazione la SD card conterrà due partizioni: boot e rootfs. Nel gestore dei dispositivi su Linux appare un menu del genere:

Anche Windows mostrerà un menu del genere: dal tuo file explorer, alla voce Questo computer vedrai le 2 partizioni.
Ora, con un editor di testo, crea sul tuo computer un file che chiamerai wpa_supplicant.conf e che editerai in questo modo:
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=«your_ISO-3166-1_two-letter_country_code»
network={
ssid="«your_SSID»"
psk="«your_PSK»"
key_mgmt=WPA-PSK
}
Dovrai sostituire le seguenti voci:
- «your_ISO-3166-1_two-letter_country_code» con l’identificativo del tuo paese (per esempio per l’Italia è IT)
- «your_SSID» con il nome SSID della tua rete WiFi
- «your_PSK» con la password della rete WiFi
A questo punto, dovrai creare un file vuoto che chiamerai ssh (senza nessuna estensione).
Le nuove distribuzioni non hanno il classico utente pi con password raspberry quindi, per poter entrare in SSH, dobbiamo provvedere in altro modo.
Con una Raspberry funzionante dobbiamo creare un file di nome userconf che conterrà l’utente che vogliamo creare con una versione criptata della password che gli vogliamo attribuire. Il formato sara quindi username:password-hash.
Supponiamo di voler tenere l’utente pi, dobbiamo creare la password-hash. Supponiamo di voler creare l’hash della password raspberry, sempre nella Raspberry dove abbiamo creato il file userconf. Dobbiamo dare il comando, da shell, seguente:
echo "raspberry" | openssl passwd -6 -stdin
Questo comando restituirà l’hash della password raspberry. Per esempio potrebbe essere una stringa così:
$6$ROOQWZkD7gkLyZRg$GsKVikua2e1Eiz3UNlxy1jsUFec4j9wF.CQt12mta/6ODxYJEB6xuAZzVerM3FU2XQ27.1tp9qJsqqXtXalLY.
Questo è l’hash della password raspberry che ho calcolato sulla mia Raspberry.
Il nostro file userconf quindi conterrà la seguente stringa:
pi:$6$ROOQWZkD7gkLyZRg$GsKVikua2e1Eiz3UNlxy1jsUFec4j9wF.CQt12mta/6ODxYJEB6xuAZzVerM3FU2XQ27.1tp9qJsqqXtXalLY.
NOTA BENE: è necessario calcolare l’hash con una Raspberry perché l’hash calcolato col computer utilizza un altro algoritmo che non consentirebbe alla Raspberry che stiamo preparando di riconoscere la password.
Alternativamente puoi scaricare dal link qua sotto il file userconf che ho creato io per avere utente pi con password raspberry.
Ora apri la partizione boot sulla SD card e copia dentro i tre files wpa_supplicant.conf, ssh e userconf. Rimuovi in sicurezza la SD card dal computer e inseriscila nella Raspberry.
Accendi la Raspberry, aspetta qualche minuto. Per poterti loggare in ssh alla Raspberry, dovrai scoprire quale sia il suo IP (quello che il router le ha assegnato tramite DHCP).
Per fare questo è sufficiente dare il comando da una shell del pc:
ping raspberrypi.local
valido sia su Linux che su Windows (previa installazione di Putty su Windows).
Nel mio PC la Raspberry risponde così:

Ciò mi fa capire che l’IP assegnato è 192.168.43.27.
Alternativamente puoi usare il tool Angry IP Scanner oppure puoi accedere alle impostazioni del tuo router per vedere i dispositivi collegati via WiFi e scoprire che IP ha la Raspberry.
Per poter loggarti sulla Raspberry in ssh dai il comando da shell (ovviamente nel tuo caso l’IP sarà diverso da questo):
con password raspberry. Su Windows è necessario Putty.
Una volta dentro la Raspberry dai i seguenti comandi per fare l’update del software:
sudo apt update
sudo apt upgrade
La password è sempre raspberry.
Configuriamo la timezone
Per configurare la timezone dai il comando:
sudo raspi-config
alla shell della Raspberry. Supponiamo di voler impostare il fuso orario di Roma (io qui farò l’esempio del fuso orario di Roma dato che vivo in Italia, tu dovrai usare il fuso orario del tuo Paese).
Apparirà una schermata così:

Seleziona l’opzione sulla localizzazione e dai Ok:

Seleziona poi l’opzione sulla timezone e dai Ok:

Seleziona ora l’area geografica e dai Ok:

Infine seleziona la città e dai Ok:

Ecco fatto!
Riavvia la Raspberry dando il comando:
sudo reboot
e, dopo pochi minuti, rientra in ssh come hai fatto prima.
Dai il comando
date
La Raspberry dovrebbe ora mostrare data e ora corrette.
Impostiamo l’IP statico
Per fare in modo che la Raspberry abbia sempre lo stesso indirizzo IP, dobbiamo impostarlo in modo che sia statico. Nei miei test l’ho impostato a 192.168.1.190. Se non facessimo così, il router le assegnerebbe un IP diverso ad ogni riavvio il che ci costringerebbe ogni volta a cambiare l’indirizzo IP nella barra del nostro browser quando accediamo all’interfaccia dell’applicazione.
Procederemo in due passi:
- imposteremo l’IP fisso nella Raspberry
- imposteremo il router in modo che riservi quell’indirizzo alla nostra Raspberry
Per il primo punto, dai il comando:
nano /etc/dhcpcd.conf
per aprire il file dhcpcd.conf ed editarlo.
Alla fine del file dovrai aggiungere un blocco del genere:
interface [INTERFACE]
static_routers=[ROUTER IP]
static domain_name_servers=[DNS IP]
static ip_address=[STATIC IP ADDRESS YOU WANT]/24
dove:
- [INTERFACE] è il nome dell’interfaccia WiFi (nel nostro caso sarà wlan0)
- [ROUTER IP] è l’indirizzo del nostro router (in genere è qualcosa del tipo 192.168.0.1 oppure 192.168.1.1). Lo puoi trovare entrando nell’interfaccia di amministrazione del tuo modem/router
- [DNS IP] è l’indirizzo del server DNS, che in genere coincide con il parametro [ROUTER IP] del modem/router
- [STATIC IP ADDRESS YOU WANT] è l’indirizzo IP che vogliamo assegnare come IP fisso alla Raspberry
Quindi, supposto che [ROUTER IP] = [DNS IP] = 192.168.1.1 e che [STATIC IP ADDRESS YOU WANT] = 192.168.1.190, il blocco avrà un aspetto del genere:
interface wlan0
static_routers=192.168.1.1
static domain_name_servers=192.168.1.1
static ip_address=192.168.1.190/24
Riavvia la Raspberry sempre col comando
sudo reboot
e poi accedi nuovamente in ssh, questa volta con IP 192.168.1.190.
Come secondo passo imposteremo il router in modo che riservi l’indirizzo 192.168.1.190 alla nostra Raspberry. Ogni modem/router è diverso dagli altri ma più o meno si somigliano. Mostrerò qui come appare il mio.
Per entrare digito l’indirizzo 192.168.1.1 (perche il mio modem ha questo IP) sul browser e, dopo aver dato la password di amministratore, arrivo alla schermata principale. Da qui devo cercare la schermata relativa al controllo degli accessi.

Ci sarà un pulsante di aggiunta di un IP statico: aggiungi l’IP scelto abbinato al MAC address della scheda WiFi della Raspberry. Ti consiglio comunque di consultare il manuale di istruzioni del tuo modem/router per questa operazione.
Verifica ora che la Raspberry si connetta alla rete dando il comando:
ping www.google.com
Se ottieni la risposta al ping la rete è connessa. Se ottieni un messaggio del tipo “Network is unreachable” dai il comando
sudo route add default gw [ROUTER IP]
dove [ROUTER IP] è il gateway che nel nostro caso è l’IP del router, cioè 192.168.1.1
Se però riavviando la Raspberry nuovamente la rete presenta il problema del tipo “Network is unreachable” procedi in questo modo:
crea uno script in /etc/dhcpcd.exit-hook per aggiungere la route del gateway predefinito col comando seguente:
sudo nano /etc/dhcpcd.exit-hook
e aggiungi questo contenuto:
#!/bin/sh
if [ "$interface" = "wlan0" ]; then
/sbin/ip route add default via 192.168.1.1 dev wlan0
fi
Rendi eseguibile lo script col comando:
sudo chmod +x /etc/dhcpcd.exit-hook
Riavvia la rete e la Raspberry:
sudo systemctl restart dhcpcd
sudo reboot
Dopo il reboot fai di nuovo il login alla Raspberry e prova nuovamente a pingare il sito di Google. Se tutto è risolto il ping dovrebbe andare a buon fine.
Se dai il comando
ip route
dovresti ottenere l’output:
default via 192.168.1.1 dev wlan0
192.168.1.0/24 dev wlan0 proto dhcp scope link src 192.168.1.190 metric 303
a conferma del successo della configurazione.
Installazione e test della webcam
A questo punto dobbiamo installare la webcam. Apri la configurazione Raspberry Pi:
sudo raspi-config
e scegli l’opzione:

si aprirà una seconda finestra dove sceglierai la voce Legacy Camera:

Conferma la scelta:

Il configuratore potrebbe avvisarci che questa modalità è deprecata e che in futuro potrebbe non essere più disponibile. Andiamo avanti e facciamo il reboot con la prossima schermata:

Una volta riavviata, rientriamo in ssh e, rimanendo nella home, testiamola con il comando:
raspistill -o test_photo.jpg
Se la camera funziona dovremmo trovarci nella home una foto appena scattata, se invece ci viene restituito un messaggio di errore la camera potrebbe essere guasta o collegata male. Per controllare la presenza della foto, diamo il comando:
ls -lah
tra i file elencati dovrebbe esserci anche il file test_photo.jpg con la sua dimensione.
Per garantirti un’installazione senza problemi e prevenire eventuali incompatibilità future dovute ad aggiornamenti del sistema operativo, ti metto a disposizione un’immagine già configurata e testata. Puoi scaricarla dal link seguente:
Per flasharla, per esempio con Balena Etcher, ti servirà una SD card da 64GB.
L’immagine include un file di configurazione WiFi generico (/etc/wpa_supplicant/wpa_supplicant.conf) che ha questo aspetto:
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=«YOUR_COUNTRY_ID»
network={
ssid="YOUR_SSID"
psk="YOUR_PASSWORD"
key_mgmt=WPA-PSK
}
Prima di utilizzare la Raspberry Pi, è necessario configurare correttamente la connessione WiFi con i dati della tua rete wireless.
Collegamento via cavo Ethernet
Collega la Raspberry Pi a una rete tramite cavo Ethernet.
Individuare l’indirizzo IP
Per scoprire l’indirizzo IP della Raspberry Pi:
Metodo 1: Usare ping raspberrypi.local (Consigliato)
Se il tuo computer è sulla stessa rete locale, puoi utilizzare il nome raspberrypi.local per scoprire l’IP della Raspberry Pi tramite mDNS (Multicast DNS).
Apri il terminale (Linux/macOS) o il prompt dei comandi (Windows) e digita:
ping raspberrypi.local
Il comando restituirà l’indirizzo IP assegnato alla Raspberry Pi, ad esempio
PING raspberrypi.local (192.168.1.100): 56 data bytes
Usa quell’indirizzo per connetterti via SSH:
La password è raspberry.
Nota per utenti Windows: Se raspberrypi.local non funziona, assicurati di avere Bonjour installato. Bonjour è incluso se hai installato iTunes.
Metodo 2: Usare l’interfaccia del router
Se ping raspberrypi.local non funziona, puoi scoprire l’indirizzo IP della Raspberry Pi dall’interfaccia web del router:
Accedi al pannello di amministrazione del tuo router digitando il suo IP nel browser (di solito 192.168.1.1 o 192.168.0.1).
Cerca nella sezione Dispositivi collegati o Client DHCP. Troverai un elenco di dispositivi connessi e il loro indirizzo IP.
Identifica la Raspberry Pi (potrebbe essere chiamata raspberrypi).
Usa quell’indirizzo IP per connetterti via SSH:
ssh pi@indirizzo_ip
Metodo 3: Collega uno schermo e una tastiera alla Raspberry Pi, accedi al terminale (con utente pi e password raspberry) e digita:
hostname -I
per scoprire l’IP assegnatole dal router.
Una volta scoperto l’indirizzo IP, accedi alla Raspberry Pi tramite SSH:
(Sostituisci 192.168.X.X con l’indirizzo IP corretto). La password predefinita è raspberry.
Modifica ora il file wpa_supplicant.conf:
sudo nano /etc/wpa_supplicant/wpa_supplicant.conf
Sostituisci i valori <<YOUR_COUNTRY_ID>>, YOUR_SSID e YOUR_PASSWORD con quelli della tua rete WiFi.
Riavvia la Raspberry Pi:
sudo reboot
NOTA: il parametro country=<<YOUR_COUNTRY_ID>> indica il codice del tuo paese e deve essere sostituito con il codice a due lettere secondo lo standard ISO 3166-1 alpha-2. Per esempio Italia → IT, Stati Uniti → US, Francia → FR. Quando sostituisci <<YOUR_COUNTRY_ID>>, rimuovi anche i simboli << e >>.
Supponendo di essere in Italia, ecco come dovrebbe apparire il file wpa_supplicant.conf dopo le modifiche:
ctrl_interface=DIR=/var/run/wpa_supplicant GROUP=netdev
update_config=1
country=IT
network={
ssid="MyWiFiNetwork"
psk="password1234"
key_mgmt=WPA-PSK
}
Se tutto è andato correttamente puoi fare il reboot della Raspberry con
sudo reboot
e scollegare il cavo LAN. Al riavvio dovresti poter essere in grado di collegarti alla Raspberry via WiFi utilizzando il suo IP fisso e il comando ssh:
con password raspberry.
Nota per l’utente
- Se non riesci a connetterti:
Verifica di essere sulla stessa rete WiFi e che la Raspberry Pi si sia effettivamente connessa. - Se hai cambiato SSID o password della rete WiFi, dovrai modificare nuovamente il file wpa_supplicant.conf tramite una connessione Ethernet.
⚠️ Nota sulla Timezone ⚠️
L’immagine di sistema ha timezone preconfigurata per l’Italia (Europe/Rome). Se sei in un’altra area geografica, è consigliato modificarla per assicurare che i log e gli orari dei file siano corretti. Segui la procedura indicata nel paragrafo “Configuriamo la timezone” per modificarla.
Installazione dell’applicazione di videosorveglianza
L’installazione del sistema di videosorveglianza è stata progettata per essere semplice e a prova di errore, grazie all’utilizzo di script automatici. Ho eliminato la necessità di interventi complessi e configurazioni manuali, lasciando agli script il compito di preparare l’ambiente, installare le dipendenze necessarie, configurare e avviare i servizi. Ogni passaggio, dall’installazione di Docker alla configurazione dei permessi dei file, viene eseguito automaticamente con pochi comandi.
Il processo segue, ovviamente, la preparazione e configurazione della Raspberry come visto nel paragrafo precedente e si articola in tre fasi principali:
- la prima fase riguarda lo scaricamento della applicazione in formato compresso zip, il suo trasferimento e decompressione nella cartella home della Raspberry;
- la seconda si occupa di installare Docker e Docker Compose, aumentando anche la memoria di swap per garantire una compilazione più stabile e veloce delle librerie Python;
- la terza fase configura il sistema di videosorveglianza vero e proprio, creando i container Docker, impostando i servizi di backend e frontend e avviando tutto il necessario per avere l’applicazione perfettamente funzionante.
Al termine della installazione il sistema è operativo e pronto all’uso, con tutti i servizi correttamente configurati. Non è richiesta nessuna esperienza avanzata in Linux o configurazione di server, solo un po’ di pazienza per eseguire gli script e seguire le semplici istruzioni.
A questo punto scarica il file di progetto dl seguente link:
e trasferiscilo nella home della Raspberry.
Se sei su Linux ti sarà sufficiente, da riga di comando, usare rsync aprendo una shell nella cartella dove si trova il file zip e dare il comando:
rsync -avzP videosurveillance.zip [email protected]:/home/pi/
Se invece sei su Windows ti suggerisco due possibilità:
1. WinSCP (Metodo consigliato)
WinSCP è un client grafico per SCP/SFTP che consente di trasferire file tra Windows e Raspberry Pi con un’interfaccia user-friendly.
Istruzioni:
- Scarica e installa WinSCP.
- Apri WinSCP e scegli il protocollo SCP o SFTP.
- Inserisci i dettagli della Raspberry Pi:
- Host: 192.168.1.190
- Nome utente: pi
- Password: quella dell’utente pi (se non l’hai cambiata ti ricordo che è raspberry)
- Una volta connesso, naviga nella tua directory locale, seleziona videosurveillance.zip e trascinalo nella cartella /home/pi sulla Raspberry Pi.
Pro: Interfaccia semplice, trasferimenti sicuri.
Contro: Richiede l’installazione di software aggiuntivo.
2. FileZilla (Metodo alternativo)
FileZilla supporta SFTP, perfetto per trasferire file sulla Raspberry Pi.
Istruzioni:
- Scarica e installa FileZilla.
- Apri FileZilla e vai su File > Site Manager.
- Configura una nuova connessione SFTP:
- Host: 192.168.1.190
- Protocollo: SFTP – SSH File Transfer Protocol
- Nome utente: pi
- Password: quella dell’utente pi (se non l’hai cambiata ti ricordo che è raspberry)
- Clicca su Connect e trascina videosurveillance.zip nella directory /home/pi.
Pro: Facile da usare, ampiamente supportato.
Contro: Può sembrare sovradimensionato per un singolo trasferimento di file.
Una volta trasferito il file entra in ssh col comando
dove, se non l’hai cambiata, la password di default è raspberry e lo troverai nella home della Raspberry dove potrai decomprimerlo con il comando:
unzip videosurveillance.zip
Verrà creata la cartella videosurveillance. Entraci col comando
cd videosurveillance
Troverai al suo interno svariati script bash e di altro tipo (per esempio script pyhton).
Prima di tutto dobbiamo installare tutto cio che riguarda Docker lanciando lo script install_docker.sh col comando
./install_docker.sh
Se non fosse già eseguibile, rendilo tale col comando:
chmod +x install_docker.sh
Una volta completata l’installazione, fai un reboot col comando
sudo reboot
Una volta riavviata la Raspberry, rientra in ssh ed entra nuovamente nella cartella di progetto videosurveillance.
Il prossimo passo è quello di lanciare lo script di installazione setup_videosurveillance.sh. Se non fosse già eseguibile, rendilo tale col comando:
chmod +x setup_videosurveillance.sh
e lancialo col comando:
./setup_videosurveillance.sh
Durante l’installazione potrebbe apparirti una schermata come questa:

Il sistema ti sta solo informando che c’è una versione più recente del kernel disponibile e suggerisce di riavviare il sistema una volta completata l’installazione, per caricare il nuovo kernel. Dai ok e prosegui.
Attendi che avvenga l’installazione. Sulla Raspberry 3 è una procedura piuttosto lunga quindi non preoccuparti.
Al termine della installazione il sistema è già avviato (vengono creati, abilitati e avviati i servizi Linux necessari). Per verificarne il comportamento puoi visualizzare i log del BE e del FE aprendo un’altra shell e loggandoti in ssh anche in questa.
Per visualizzare il log del BE dai il comando
docker logs videosurveillance_backend -f
nella prima shell e il comando
docker logs videosurveillance_frontend -f
nella seconda shell per visualizzare il log del FE.
I pilastri dell’applicazione: Motion e Docker
Introduzione a Motion e configurazione del rilevamento di movimento
Motion è un software open-source ampiamente utilizzato per il rilevamento di movimento tramite videocamera, particolarmente apprezzato in progetti di videosorveglianza grazie alla sua flessibilità e capacità di essere personalizzato secondo le esigenze dell’utente. Motion si basa su un file di configurazione chiamato motion.conf, che permette di controllare ogni aspetto del rilevamento di movimento, dalla sensibilità alla frequenza degli scatti, fino al livello di logging per il monitoraggio degli eventi.
Questo file di configurazione rappresenta il cuore di Motion, poiché racchiude i parametri che determinano non solo la qualità del rilevamento ma anche la gestione dei falsi positivi e l’organizzazione delle immagini catturate. Ogni parametro in motion.conf gioca un ruolo fondamentale nel bilanciare la sensibilità del software, evitando inutili notifiche per eventi minimi e assicurando al contempo che ogni attività significativa sia rilevata e documentata.
Essendo pensato per l’uso in ambienti molto diversi (interni, esterni, con luce variabile, ecc.), Motion offre un’ampia gamma di opzioni. In contesti come una veranda, dove le condizioni di luce possono variare durante il giorno e piccoli movimenti (ad esempio foglie mosse dal vento) possono essere scambiati per eventi, è fondamentale trovare la giusta calibrazione per evitare sovraccarichi di foto e notifiche. La configurazione dei parametri in motion.conf consente di definire soglie di sensibilità e modalità di reazione specifiche, come il numero di frame consecutivi necessari per confermare un movimento o il filtro da applicare per ridurre il rumore di fondo.
Di seguito, esploreremo i principali parametri di motion.conf con l’obiettivo di ottimizzare il rilevamento per ambienti dinamici, fornendo suggerimenti su come evitare falsi positivi e ottenere risultati affidabili anche in condizioni di illuminazione variabile. Questi parametri possono essere adattati per ottenere una configurazione che soddisfi le esigenze specifiche di ogni progetto di videosorveglianza, sia esso professionale o domestico.
Un esempio del file motion.conf utilizzato in questo progetto è il seguente:
daemon on
log_level 6
output_pictures on
ffmpeg_output_movies off
target_dir /app/photos/motion
threshold 4000
minimum_motion_frames 10
event_gap 15
noise_level 64
noise_tune on
despeckle_filter EedDl
auto_brightness on
Parametri di base per il rilevamento di movimento
- daemon on: fa eseguire Motion in background come daemon. Importante per avviare Motion come servizio autonomo.
- log_level 6: Imposta il livello di logging; un valore più alto fornisce maggiori dettagli su ciò che accade durante l’esecuzione, utile per debug.
- output_pictures on: abilita il salvataggio delle immagini al rilevamento di movimento. Questo parametro deve essere impostato su “on” per ottenere scatti fotografici ogni volta che Motion rileva un cambiamento significativo.
- ffmpeg_output_movies off: disabilita la creazione di video in uscita, utile per risparmiare spazio su disco quando si vogliono solo immagini.
- nel caso si volessero catturare video, le impostazioni saranno output_pictures off e ffmpeg_output_movies on
Sensibilità e falsi positivi
- threshold 2000: questo valore rappresenta la sensibilità al movimento. Un valore più basso aumenta la sensibilità, mentre un valore più alto la riduce. Con un threshold alto, Motion ignorerà piccoli cambiamenti nella scena, riducendo i falsi positivi. Consigliato: tra 2000 e 4000.
- minimum_motion_frames 3: definisce il numero minimo di frame consecutivi che devono mostrare movimento per considerarlo reale. Impostare questo parametro più alto può ridurre i falsi positivi, poiché richiede movimenti più duraturi.
- event_gap 10: stabilisce l’intervallo di tempo (in secondi) tra eventi di movimento successivi. Un valore più alto riduce le foto duplicate per un unico evento, riducendo l’output a raffica.
- noise_level 128: serve per ridurre il rumore di fondo delle immagini. Più alto è il valore, maggiore è la tolleranza al rumore video.
- noise_tune on: adatta automaticamente la sensibilità di Motion al livello di rumore dell’ambiente, utile in scenari dove le condizioni di luce possono cambiare, come in una veranda.
- despeckle_filter EedDl: questo filtro rimuove i piccoli cambiamenti, come il rumore, riducendo ulteriormente i falsi positivi. Le lettere indicano i tipi di filtro che si applicano, e in genere una sequenza “EedDl” è efficace per ambienti con rumore.
- auto_brightness on: consente a Motion di adattarsi ai cambiamenti di luce ambientale, utile in ambienti esterni o aree con illuminazione variabile.
Gestione dell’output e rotazione delle immagini
- target_dir /app/photos/motion: specifica la directory di destinazione per le foto. È consigliato mantenere una struttura di cartelle organizzata per evitare l’accumulo di file non gestiti. Nel caso il sistema fosse impostato per la cattura di video, il parametro target_dir sarebbe impostato al valore /app/videos/motion
- max_mpeg_time 0: impostato a zero per evitare limiti di tempo nei video.
Il file motion.conf si trova nella cartella di sistema videosurveillance ma non è necessario modificarlo a mano (anzi è altamente sconsigliato). La sua configurazione è possibile (come vedremo in seguito) tramite la pagina di configurazione dell’interfaccia web mostrata dal FE.
Docker: una piattaforma per semplificare lo sviluppo e la distribuzione
Docker è una piattaforma open-source progettata per automatizzare la distribuzione di applicazioni attraverso container leggeri. Un container è un’unità software che incapsula un’applicazione e le sue dipendenze, garantendo che possa essere eseguita ovunque, indipendentemente dall’ambiente. Questo rende Docker uno strumento estremamente potente per lo sviluppo e la distribuzione di applicazioni complesse, come il nostro sistema di videosorveglianza basato su Raspberry Pi.
Vantaggi di Docker nel nostro progetto
- Isolamento delle dipendenze: Docker consente di impacchettare tutte le dipendenze (come Python, librerie specifiche, ecc.) necessarie per il funzionamento del sistema di videosorveglianza. Questo evita problemi di compatibilità che possono verificarsi su diversi sistemi operativi.
- Facilità di distribuzione: creare un container Docker ci permette di eseguire il progetto su qualsiasi dispositivo che supporta Docker, che sia un Raspberry Pi, un PC, o un server remoto. Non importa dove viene eseguito, l’applicazione funzionerà allo stesso modo.
- Riproducibilità: con Docker, possiamo garantire che il sistema venga eseguito esattamente come previsto ogni volta che viene avviato. Questo elimina errori causati da differenze nell’ambiente di sviluppo.
- Semplicità di aggiornamento: quando aggiorniamo o miglioriamo il progetto, possiamo creare una nuova versione del container senza dover aggiornare manualmente ogni componente del sistema. I container possono essere aggiornati facilmente, distribuendo una nuova immagine.
Come utilizziamo Docker nel nostro progetto
Nel nostro progetto di videosorveglianza, Docker gestisce l’applicazione che cattura le foto o i video, genera miniature, invia notifiche tramite Pushover/Telegram/Gmail, e fornisce un’API per l’interazione con il sistema. Ecco i passaggi principali che abbiamo seguito per utilizzare Docker:
- Dockerfile: il file Dockerfile definisce l’ambiente in cui verrà eseguita l’applicazione. Include la base del sistema operativo (ad esempio, una versione leggera di Python), le librerie richieste (come Pillow per l’elaborazione delle immagini) e il codice sorgente del progetto. Questo è il cuore del container.
- Creazione dell’immagine: con Docker, costruiamo un’immagine basata sul Dockerfile, che include tutto il necessario per eseguire l’applicazione. L’immagine è una sorta di “istantanea” del sistema che esegue il nostro software.
- Esecuzione del container: una volta creata l’immagine, possiamo avviare il container che eseguirà l’applicazione. Ogni volta che il sistema parte, esegue l’applicazione esattamente come è stato configurato nel Dockerfile.
- Docker Compose: Docker Compose ci consente di gestire facilmente più container e configurazioni. In questo progetto, ad esempio, potrebbe essere utilizzato per eseguire simultaneamente l’applicazione di videosorveglianza e altri servizi correlati, come un database separato o altri componenti del sistema.
Perché usare Docker nel nostro progetto?
- Automazione: Docker semplifica enormemente la distribuzione e l’automazione del progetto. Invece di configurare manualmente ogni componente del sistema su ogni nuovo dispositivo Raspberry Pi, possiamo semplicemente distribuire il container Docker pre-configurato.
- Risparmio di tempo: con Docker, non è necessario installare manualmente tutte le dipendenze su ogni macchina, il che riduce il tempo necessario per mettere in funzione il sistema.
- Modularità: possiamo gestire diverse parti del progetto (ad esempio, la cattura delle immagini, la gestione delle miniature e le notifiche) in container separati e farli comunicare tra loro tramite rete interna, garantendo una maggiore flessibilità e facilità di gestione.
Docker è quindi uno strumento essenziale per garantire la scalabilità, la riproducibilità e la facilità di gestione del nostro progetto di videosorveglianza su Raspberry Pi. Che si tratti di un singolo dispositivo o di più sistemi, Docker ci offre una piattaforma robusta e modulare per implementare e gestire il software senza complicazioni.
Docker vs. Macchina Virtuale: le differenze chiave
Spesso Docker viene confuso con una macchina virtuale (VM) come quelle create con software come VMware o VirtualBox, ma ci sono differenze fondamentali tra i due approcci. Mentre entrambi offrono l’isolamento di applicazioni e sistemi operativi, lo fanno in modi completamente diversi, con implicazioni significative per le prestazioni, la gestione e l’efficienza.
1. Architettura
- Macchina Virtuale (VM): una VM è un intero sistema operativo virtualizzato che gira su un hypervisor (come VMware o VirtualBox), il quale, a sua volta, gira sopra il sistema operativo host. Ogni VM contiene un’installazione completa del sistema operativo (Windows, Linux, ecc.), le sue librerie e dipendenze, oltre all’applicazione stessa. Questo significa che una VM richiede un’intera “copia” virtuale del sistema operativo, incluso il kernel, i driver, e tutte le risorse di sistema.
- Schema semplificato di una VM:
- Hardware
- Sistema operativo host (es. Windows, Linux)
- Hypervisor (es. VMware, VirtualBox)
- Sistema operativo guest (es. Linux, Windows)
- Applicazione e dipendenze
- Docker: Docker, invece, utilizza i container, che condividono lo stesso kernel del sistema operativo host. Invece di virtualizzare un intero sistema operativo, Docker virtualizza solo l’ambiente dell’applicazione, isolando le dipendenze e l’applicazione stessa. Ciò rende Docker molto più leggero rispetto a una VM perché non c’è bisogno di un intero sistema operativo separato per ciascun container.
- Schema semplificato di Docker:
- Hardware
- Sistema operativo host (es. Linux, Windows)
- Docker engine
- Container (applicazione + dipendenze)
2. Prestazioni e risorse
- Macchine Virtuali: poiché una VM deve eseguire un intero sistema operativo, consuma molte più risorse. Richiede CPU, RAM e spazio su disco per supportare sia l’hypervisor che il sistema operativo virtualizzato. Ogni VM può “pesare” parecchi gigabyte e le prestazioni possono essere rallentate a causa dell’overhead dell’hypervisor e delle risorse duplicate.
- Docker: i container sono estremamente leggeri. Poiché condividono il kernel del sistema operativo host e non devono eseguire un intero sistema operativo guest, l’overhead è minimo. Questo significa che puoi eseguire molti più container su una singola macchina rispetto alle VM, con un utilizzo delle risorse molto più efficiente.
3. Avvio e gestione
- Macchine Virtuali: l’avvio di una VM può richiedere minuti, poiché è necessario avviare l’intero sistema operativo guest e i suoi servizi. Ogni volta che si avvia una VM, è come avviare un computer completo. La gestione delle VM può essere complessa, con la necessità di aggiornamenti del sistema operativo e la configurazione manuale di driver, reti, ecc.
- Docker: i container Docker sono progettati per essere veloci. Poiché si basano sul kernel del sistema host e non devono avviare un intero sistema operativo, l’avvio di un container richiede solo pochi secondi. Inoltre, Docker semplifica la gestione dei container attraverso strumenti come Docker Compose, che consente di gestire più container contemporaneamente con facilità.
4. Portabilità
- Macchine Virtuali: una VM può essere trasferita e eseguita su un’altra macchina, ma a causa delle sue dimensioni e della complessità del sistema operativo virtualizzato, il processo non è sempre semplice o veloce. Ogni VM include un’intera installazione del sistema operativo, il che la rende molto più pesante e meno portatile.
- Docker: Docker è progettato per la portabilità. Poiché i container sono leggeri e includono solo le dipendenze essenziali per l’applicazione, possono essere facilmente trasferiti e avviati su qualsiasi macchina che supporta Docker, indipendentemente dal sistema operativo host. Questa portabilità è uno dei principali vantaggi di Docker, permettendo di eseguire applicazioni senza preoccuparsi delle differenze tra ambienti.
5. Sicurezza
- Macchine Virtuali: le VM offrono un elevato grado di isolamento poiché ogni macchina virtuale ha il proprio sistema operativo separato. Se una VM viene compromessa, l’hypervisor e le altre VM rimangono generalmente al sicuro, poiché non condividono risorse dirette.
- Docker: anche Docker offre isolamento, ma poiché i container condividono lo stesso kernel del sistema operativo host, esiste un rischio di sicurezza maggiore rispetto alle VM, soprattutto se il container viene eseguito con privilegi elevati. Detto questo, Docker ha adottato molte misure di sicurezza, come l’uso di namespace e cgroup, per mitigare questi rischi.
6. Utilizzo nel progetto di videosorveglianza
Nel contesto del nostro progetto di videosorveglianza, Docker offre un vantaggio significativo rispetto alle VM. Invece di configurare una macchina virtuale completa con un sistema operativo separato, Docker ci permette di eseguire un container leggero che contiene solo l’applicazione e le sue dipendenze, garantendo prestazioni ottimali anche su hardware limitato come il Raspberry Pi. La portabilità di Docker significa che possiamo sviluppare il sistema su una macchina e distribuirlo facilmente su altre senza dover riconfigurare ogni volta l’ambiente. Questo rende Docker perfetto per un progetto che richiede flessibilità, portabilità e facilità di gestione.
Riepilogo delle differenze
Caratteristica | Docker | Macchina Virtuale |
---|---|---|
Isolamento | Isolamento a livello di processo e kernel | Isolamento completo a livello di sistema operativo |
Utilizzo delle risorse | Molto leggero (condivide il kernel del sistema host) | Pesante (ogni VM esegue un sistema operativo completo) |
Avvio | Avvio in pochi secondi | Avvio in minuti (dipende dal SO) |
Portabilità | Altamente portabile | Portabilità limitata e più lenta |
Sicurezza | Isolamento buono, ma con rischi se mal configurato | Elevato isolamento grazie all’hypervisor |
Abilitazione e configurazione delle notifiche e delle comunicazioni in tempo reale
Pushover: notifiche in tempo reale per il tuo progetto di videosorveglianza
Pushover è una piattaforma di notifica push progettata per inviare messaggi istantanei a dispositivi mobili, tablet e desktop. È particolarmente utile in scenari come il nostro progetto di videosorveglianza, dove è fondamentale ricevere alert in tempo reale, ad esempio quando viene rilevato un movimento o caricata una nuova immagine.
Come funziona Pushover?
- Creazione di un account: per iniziare, è necessario registrarsi su Pushover.net. Dopo aver creato l’account, puoi scaricare l’app Pushover disponibile per Android, iOS e come client desktop.
- Registrazione di un’applicazione: una volta registrato, puoi creare una nuova “applicazione” direttamente dal tuo account. Questa applicazione rappresenterà il progetto di videosorveglianza. Ogni applicazione ha un API Token, un codice univoco che utilizzerai per autenticare e inviare notifiche al tuo dispositivo.
- Configurazione del token: dopo aver creato l’applicazione, ti verrà assegnato un API Token. Questo sarà necessario per integrare Pushover nel codice Python. Ogni volta che invierai una notifica, il tuo script includerà questo token, assieme al User Key del destinatario (può essere trovato nella sezione utente del tuo account Pushover).
- Costo e limiti:
- Pushover offre un periodo di prova gratuito di 30 giorni durante il quale è possibile inviare fino a 7500 notifiche al mese.
- Dopo il periodo di prova (al momento in cui questo articolo viene redatto) viene richiesto un pagamento una tantum di 5 dollari per piattaforma (Android o iOS). Non ci sono abbonamenti ricorrenti, e questa licenza consente l’invio illimitato di notifiche (fino a 7500 al mese).
- È possibile ricevere notifiche su più dispositivi contemporaneamente, collegati allo stesso account.
- Invio delle notifiche:
- Pushover supporta vari tipi di notifiche, tra cui messaggi di testo semplici, notifiche con immagini (ad esempio le miniature delle foto scattate dalla videocamera) e link ipertestuali.
- Nel tuo progetto, puoi includere la miniatura della foto come allegato alla notifica, permettendo all’utente di vedere direttamente l’immagine in tempo reale sul proprio telefono.
- La notifica può anche contenere un titolo personalizzato, un messaggio dettagliato e il link alla foto caricata.
- Sicurezza: le notifiche inviate tramite Pushover sono criptate end-to-end, garantendo che i tuoi messaggi e immagini siano trasmessi in modo sicuro. Inoltre, puoi personalizzare le priorità delle notifiche, decidendo se devono apparire come alert critici o messaggi normali.
Integrazione nel progetto
Nel nostro progetto, Pushover può essere utilizzato per notificare l’utente ogni volta che viene caricata una nuova foto o quando viene rilevato un movimento. Una volta configurato il token e l’API, puoi inviare una notifica direttamente dal codice Python come segue:
import requests
def send_notification(api_token, user_key, message, image_path=None):
data = {
"token": api_token,
"user": user_key,
"message": message,
}
files = {"attachment": open(image_path, "rb")} if image_path else None
response = requests.post("https://api.pushover.net/1/messages.json", data=data, files=files)
return response
Il messaggio e l’immagine (ad esempio la miniatura creata) verranno inviati al dispositivo registrato su Pushover.
Pro e contro di Pushover
Pro:
- Semplice e immediato: Pushover è estremamente facile da configurare e utilizzare, rendendolo ideale per progetti DIY come il nostro.
- Costo contenuto: la licenza una tantum di 5 dollari per piattaforma è un’opzione conveniente rispetto ad altri servizi di notifica a pagamento ricorrente.
- Compatibilità: funziona su Android, iOS e desktop, permettendo di ricevere notifiche su qualsiasi dispositivo.
Contro:
- Limite gratuito: il periodo di prova di 30 giorni è gratuito, ma per l’uso continuo dopo il limite mensile o alla fine del periodo di prova, è necessario pagare.
- Implicazione di un account: l’utente finale deve comunque creare un account su Pushover e scaricare l’applicazione per ricevere le notifiche.
Vediamo piu in dettaglio il processo di registrazione.
Processo di registrazione a Pushover
Creazione dell’account:
- Vai su Pushover.net e clicca su “Sign Up” per registrarti.
- Inserisci una email valida e scegli una password sicura.
- Dopo aver compilato questi dati, riceverai un’email di conferma. Clicca sul link contenuto nell’email per verificare il tuo indirizzo.
Verifica dell’email:
- Una volta confermata l’email, sarai reindirizzato alla pagina principale del tuo account, come mostrato nell’immagine seguente.
- In questa schermata, puoi vedere il tuo User Key, il codice univoco associato al tuo account. Questo User Key è importante perché lo userai per ricevere le notifiche sul tuo dispositivo. Potrai fornirlo anche alle applicazioni che devono inviarti notifiche tramite Pushover.

Pagina di gestione dell’account: nell’immagine precedente si vedono diverse sezioni importanti che meritano una spiegazione dettagliata:
- Your User Key: questo è il codice univoco che identifica il tuo account su Pushover. Lo utilizzerai nel codice del tuo progetto per inviare notifiche al dispositivo collegato al tuo account. Questo codice deve essere protetto, poiché è l’identificatore per ricevere notifiche sul tuo dispositivo.
- Devices: in questa sezione vengono elencati tutti i dispositivi su cui hai installato l’app di Pushover. In questo caso, vedi che il dispositivo Android è collegato. Puoi aggiungere altri dispositivi, come tablet o desktop, cliccando su Add Phone, Tablet, or Desktop. Se più dispositivi sono registrati, puoi scegliere su quale dispositivo specifico inviare le notifiche, oppure inviarle a tutti contemporaneamente.
- E-Mail Aliases: questa funzione permette di ricevere notifiche direttamente da email inviate a un alias Pushover, come ad esempio [email protected]. Le email inviate a questo indirizzo verranno trasformate in notifiche push e recapitate sui dispositivi collegati al tuo account. È una funzione utile per trasformare le email in notifiche veloci e personalizzate.
- Applications: qui puoi creare nuove applicazioni. Ogni applicazione avrà un API Token, che è necessario per l’invio delle notifiche dal tuo progetto o servizio. L’applicazione ha un limite di 10.000 messaggi al mese (notifiche gratuite).
- Usage Statistics: puoi monitorare il numero di notifiche inviate tramite l’applicazione, suddivise tra Free Messages (notifiche gratuite) e Paid Messages (notifiche pagate).
Creazione di una nuova applicazione/API Token su Pushover
Dopo aver completato la registrazione e l’installazione dell’app di Pushover, il passo successivo è creare una nuova applicazione. Questo ti permetterà di ricevere notifiche push automatiche dal tuo progetto o sito web direttamente sul tuo dispositivo.

Nell’immagine sopra, viene mostrato il modulo per la creazione di una nuova applicazione/API Token su Pushover. Di seguito, spieghiamo i vari campi e i passaggi per creare correttamente la tua applicazione.
Perché è necessario creare un’applicazione?
- Scopo: l’applicazione che crei genererà un API Token unico. Questo token è essenziale per permettere a Pushover di inviare notifiche push ai dispositivi registrati. Ogni applicazione può inviare fino a 10.000 notifiche gratuite al mese.
- Integrazione con progetti: creare un’API Token è un passaggio obbligatorio per integrare il tuo sistema di notifiche push, che sia per un progetto di videosorveglianza, un servizio di monitoraggio, o qualsiasi altra automazione.
Campi del modulo
- Name (Nome):
- Inserisci un nome per la tua applicazione. Il nome dovrebbe essere breve e descrittivo (ad esempio: “Videosorveglianza”, “Notifiche Monitoraggio”).
- Questo nome verrà mostrato come titolo di default nelle notifiche inviate, se non diversamente specificato.
- Description (Descrizione) (opzionale):
- Puoi inserire una descrizione della tua applicazione. Questo campo è opzionale, ma può aiutarti a ricordare il contesto o lo scopo dell’applicazione, soprattutto se ne crei più di una.
- URL (opzionale):
- Se la tua applicazione è associata a un sito web o una repository GitHub, puoi inserire l’URL qui. Questo è particolarmente utile per app pubbliche o plugin.
- Icon (Icona):
- È possibile caricare un’icona personalizzata per la tua applicazione, che verrà visualizzata insieme alle notifiche. L’icona deve essere in formato PNG con dimensioni 72×72 pixel e preferibilmente con uno sfondo trasparente. Se carichi un’immagine di dimensioni diverse, verrà ridimensionata automaticamente.
- È un dettaglio estetico che può migliorare la presentazione delle notifiche, rendendole visivamente riconoscibili.
- Accettazione dei Termini di Servizio:
- Prima di creare l’applicazione, devi accettare i Termini di Servizio di Pushover. Assicurati di spuntare la casella.
Passaggi finali
- Dopo aver compilato i campi richiesti, clicca su Create Application. Una volta fatto, verrà generato il tuo API Token, che sarà visibile nella pagina di gestione dell’applicazione.
- Questo API Token è quello che userai nel tuo codice per integrare Pushover e inviare notifiche push automatizzate.
La creazione di un’applicazione su Pushover è un passo cruciale per inviare notifiche push automatizzate. Utilizzando l’API Token generato, puoi configurare i tuoi script Python (o altri linguaggi) per inviare notifiche in tempo reale direttamente ai dispositivi collegati al tuo account Pushover.
In sintesi, Pushover è uno strumento pratico e versatile che può facilmente essere integrato in progetti di automazione e sorveglianza come il nostro, garantendo notifiche rapide ed efficaci su dispositivi mobili e desktop.
Configurazione di Gmail per le notifiche via mail
Per poter attivare le notifiche tramite Gmail bisogna, ovviamente, avere una email Gmail.
Per configurare l’invio di notifiche via email tramite Gmail, è necessario attivare l’autenticazione a due fattori e creare una “password per app”. Questa password speciale sarà utilizzata dal sistema per accedere al tuo account Gmail senza compromettere la sicurezza e senza usare la tua password della tua gmail.
Passaggi per configurare l’account Gmail
- Attiva l’Autenticazione a Due Fattori per l’Account Gmail
- Accedi al tuo account Gmail: https://mail.google.com.
- Visita la sezione di gestione della sicurezza del tuo account: https://myaccount.google.com/security.
- Trova la sezione “Come accedi a Google” e seleziona Verifica in due passaggi.
- Segui le istruzioni per attivare la verifica a due fattori, scegliendo un metodo di autenticazione (es. SMS, app di autenticazione).
- Una volta attivata, verrai riportato alla pagina di sicurezza con la verifica in due passaggi abilitata.
Crea una Password per l’App
- Dalla stessa pagina di sicurezza, cerca la sezione “Password per le app” (visita https://myaccount.google.com/apppasswords).
- Inserisci un nome descrittivo per questa password, come “Videosorveglianza” o “RaspberryPi”, per riconoscerla facilmente.
- Clicca su Crea. Google ti fornirà una password per l’app formata da 16 caratteri, separati in blocchi di quattro lettere (es. abcd efgh ijkl mnop).
- Annota questa password e assicurati di copiarla correttamente (con o senza spazi, la versione intera sarà accettata nel sistema di configurazione).
Nel file config.ini, alla sezione [notification] troverai una parte fatta così:
gmail_enabled = true
smtp_server = smtp.gmail.com
smtp_port = 587
sender_email = [email protected]
app_password = gkmyabypokmn
receiver_email = [email protected]
Potresti modificarla con i tuoi dati (lasciando invariati i campi smtp_server e smtp_port) ma è una operazione altamente sconsigliata modificare a mano i files di configurazione dell’applicazione. Per questo genere di operazioni potrai utilizzare l’apposita sezione nella pagina web di configurazione, quindi da questo momento in poi usa solo quella.
Come creare un bot Telegram
Telegram è un’applicazione di messaggistica istantanea e VoIP che può essere installata sul tuo smartphone (Android e iPhone) o computer (PC, Mac e Linux). Telegram ti consente di creare bot con cui il nostro dispositivo può interagire.
Creiamo ora il nostro bot!
Se non hai già Telegram, installalo e poi cerca il bot botFather. Fai clic sull’elemento visualizzato. Apparirà la seguente schermata:

Digita il comando /start per leggere le istruzioni:

Ora digita il comando /newbot per creare il tuo bot. Dagli un nome e uno username:

Se il tuo bot è stato creato con successo, riceverai un messaggio con un link per accedere al bot e al token del bot.
Salva il token del bot perché ti servirà in seguito affinché la board possa interagire con il bot.
Ecco come appare la schermata in cui è scritto il token del bot:

A questo punto devi individuare il tuo ID utente Telegram. Ma…..come troviamo questo ID?
Nel tuo account Telegram, cerca IDBot e avvia una conversazione con quel bot:

Quindi digita il comando /getid e lui ti risponderà col tuo ID:

A questo punto abbiamo creato il nostro bot e abbiamo tutti gli elementi per interfacciarlo con il nostro dispositivo: lo username, il token e lo userid. Questi dati li useremo per inserirli nel file di configurazione config.ini.
Gli script bash di gestione
Per semplificare la gestione del sistema di videosorveglianza, sono stati sviluppati diversi script Bash che permettono di eseguire operazioni di manutenzione e amministrazione con un solo comando. Questi script eliminano la necessità di interventi manuali complessi, garantendo una maggiore efficienza, sicurezza e facilità d’uso per l’utente.
Grazie a questi strumenti, è possibile eseguire rapidamente operazioni come:
- pulizia e gestione dei container Docker
- ripristino di backup
- gestione del database
- spegnimento sicuro della Raspberry Pi
- configurazione automatizzata del sistema
- installazione di WireGuard per l’accesso da rete esterna
Di seguito, una panoramica dettagliata di ogni script e delle operazioni che svolge.
Gli script sono:
- clean_data.sh
- hard_clean_docker.sh
- poweroff_raspberry.sh
- reset_admin_db.sh
- restore_backup.sh
- setup_videosurveillance.sh
- install_docker.sh
- soft_clean_docker.sh
- install_wireguard.sh
Lo script clean_data.sh
Lo script clean_data.sh ha il compito di eliminare tutti i dati del sistema di videosorveglianza, ripristinando l’ambiente a uno stato “pulito”. Questo significa che tutte le foto, video e il database verranno rimossi definitivamente. Di seguito vediamo come è fatto:
#!/bin/bash
# Stop the service
echo "Service shutdown videosurveillance.service..."
sudo systemctl stop videosurveillance.service || {
echo "Error stopping service."
exit 1
}
# Delete the photos, videos and database folders on the container and host
echo "Clean up photos, videos and database folders..."
# Removes photos, thumbnails and videos
rm -rf ./photos/*
rm -rf ./videos/*
# Removes the database
rm -f ./data/videosurveillance_db.sqlite
# Restores an empty version of the database
cp -f /home/pi/videosurveillance/empty_videosurveillance_db.sqlite /home/pi/videosurveillance/data/videosurveillance_db.sqlite
sudo chown -R root:root data
# Restarting the service
echo "Restarting the service videosurveillance.service..."
sudo systemctl start videosurveillance.service || {
echo "Error starting service."
exit 1
}
echo "Cleaning completed."
La riga
#!/bin/bash
specifica che lo script deve essere eseguito utilizzando Bash.
Il blocco
# Stop the service
echo "Service shutdown videosurveillance.service..."
sudo systemctl stop videosurveillance.service || {
echo "Error stopping service."
exit 1
}
ferma il servizio videosurveillance.service.
La riga seguente stampa un messaggio che indica che l’operazione di pulizia è in corso:
# Delete the photos, videos and database folders on the container and host
echo "Clean up photos, videos and database folders..."
Le righe successive:
# Removes photos, thumbnails and videos
rm -rf ./photos/*
rm -rf ./videos/*
eliminano tutti i file all’interno della cartella photos/, comprese le miniature delle immagini e tutti i file nella cartella videos/
, compresi i video e le loro miniature.
La riga
# Removes the database
rm -f ./data/videosurveillance_db.sqlite
elimina il file del database SQLite che contiene i metadati delle immagini e dei video.
Il blocco
# Restores an empty version of the database
cp -f /home/pi/videosurveillance/empty_videosurveillance_db.sqlite /home/pi/videosurveillance/data/videosurveillance_db.sqlite
sudo chown -R root:root data
ripristina una versione vuota del database ma con la struttura già creata e gli assegna l’owner e il gruppo corretti.
Il blocco
# Restarting the service
echo "Restarting the service videosurveillance.service..."
sudo systemctl start videosurveillance.service || {
echo "Error starting service."
exit 1
}
fa ripartire il servizio videosurveillance.service.
Mentre la riga
echo "Cleaning completed."
stampa un messaggio per confermare che l’operazione di pulizia è terminata.
⚠️ Attenzione!
- questo script non chiede conferma prima di cancellare i dati. Una volta eseguito, le immagini, i video e il database saranno permanentemente eliminati;
- è consigliato eseguire un backup prima di lanciare questo script se si vogliono conservare i dati.
Utilizzo
Per eseguire lo script dare il comando:
./clean_data.sh
Se non è eseguibile, renderlo tale con:
chmod +x clean_data.sh
Questo script è particolarmente utile quando si vuole ripartire da zero senza dover eliminare manualmente i file.
Lo script hard_clean_docker.sh
Lo script hard_clean_docker.sh esegue una pulizia completa e distruttiva dell’ambiente Docker e dei file di sistema del progetto di videosorveglianza.
Verranno eliminati tutti i container, le immagini, le reti, i volumi Docker e i servizi di sistema relativi alla videosorveglianza. Inoltre, verranno cancellati tutti i file e database, compresi backup e log.
⚠️ ATTENZIONE! Questo script ripristina l’ambiente a uno stato completamente pulito, come se il sistema non fosse mai stato installato. Non è possibile recuperare i dati eliminati!
#!/bin/bash
echo "Running Docker clean hard... Warning: it will delete everything!"
# Confirmation by the user
read -p "Do you want to continue? This action will delete all containers, images, networks, Docker volumes and Linux services! [y/N]: " response
if [[ "$response" != "y" && "$response" != "Y" ]]; then
echo "Operation cancelled by user."
exit 0
fi
# Stop Linux services
for service in videosurveillance.service host_service.service; do
echo "### Stopping the service $service ###"
if systemctl is-active --quiet "$service"; then
sudo systemctl stop "$service"
echo "Service $service stopped."
else
echo "Service $service was not running."
fi
done
# Initial state
echo "### Initial state of Docker resources ###"
docker system df
# Stop all containers
echo "### Stopping all containers... ###"
docker stop $(docker ps -aq) || echo "No containers running."
# Delete all containers
echo "### Removing all containers... ###"
docker rm $(docker ps -aq) || echo "No containers to remove."
# Delete all images
echo "### Removing all images... ###"
docker rmi $(docker images -q) || echo "No images to remove."
# Delete all non-default networks
echo "### Removing all non-default Docker networks... ###"
docker network prune -f || echo "No custom networks to remove."
# Delete all volumes
echo "### Removing all Docker volumes... ###"
docker volume prune -f || echo "No volumes to remove."
# Final state
echo "### Final state of Docker resources ###"
docker system df
# Remove Linux Services
for service in videosurveillance.service host_service.service; do
echo "### Removing the service $service ###"
if [ -f "/etc/systemd/system/$service" ]; then
sudo systemctl disable "$service"
sudo rm "/etc/systemd/system/$service"
echo "Service $service successfully removed."
else
echo "The $service service did not exist."
fi
done
# Reload systemd to remove any residual references
sudo systemctl daemon-reload
echo "Clean up photos, videos and database folders..."
# Removes photos, thumbnails and videos
sudo rm -rf ./photos/*
sudo rm -rf ./videos/*
# Removes the database
sudo rm -f ./data/videosurveillance_db.sqlite
# Remove the backup
echo "Cleaning backups..."
sudo rm ./backup.zip
sudo rm ./db_dump.db
# Removes the log
echo "Cleaning the log..."
sudo rm ./restore_backup.log
echo "File cleanup completed."
echo "Hard cleanup completed. All Docker and system data have been reset."
Le righe
#!/bin/bash
echo "Running Docker clean hard... Warning: it will delete everything!"
specificano l’uso della shell Bash per l’esecuzione dello script e visualizzano un messaggio di avviso per l’utente.
Le righe
read -p "Do you want to continue? This action will delete all containers, images, networks, Docker volumes and Linux services! [y/N]: " response
if [[ "$response" != "y" && "$response" != "Y" ]]; then
echo "Operation cancelled by user."
exit 0
fi
chiedono all’utente di confermare digitando y
(o Y
) per procedere. Se l’utente preme qualunque altro tasto, l’operazione viene annullata e lo script si interrompe.
Le righe
for service in videosurveillance.service host_service.service; do
echo "### Stopping the service $service ###"
if systemctl is-active --quiet "$service"; then
sudo systemctl stop "$service"
echo "Service $service stopped."
else
echo "Service $service was not running."
fi
done
arrestano i servizi videosurveillance.service e host_service.service se attivi. Se un servizio non è in esecuzione, lo script lo segnala.
Le righe
echo "### Initial state of Docker resources ###"
docker system df
mostrano l’uso attuale delle risorse Docker (immagini, volumi, container, reti) prima della pulizia.
Le righe
echo "### Stopping all containers... ###"
docker stop $(docker ps -aq) || echo "No containers running."
echo "### Removing all containers... ###"
docker rm $(docker ps -aq) || echo "No containers to remove."
echo "### Removing all images... ###"
docker rmi $(docker images -q) || echo "No images to remove."
echo "### Removing all non-default Docker networks... ###"
docker network prune -f || echo "No custom networks to remove."
echo "### Removing all Docker volumes... ###"
docker volume prune -f || echo "No volumes to remove."
- arrestano e rimuovono tutti i container Docker
- cancellano tutte le immagini Docker
- eliminano tutte le reti personalizzate di Docker
- rimuovono tutti i volumi Docker
Se una delle risorse non esiste, viene mostrato un messaggio senza generare errori.
Dopo la pulizia, lo script mostra lo stato aggiornato delle risorse Docker:
echo "### Final state of Docker resources ###"
docker system df
Le righe
for service in videosurveillance.service host_service.service; do
echo "### Removing the service $service ###"
if [ -f "/etc/systemd/system/$service" ]; then
sudo systemctl disable "$service"
sudo rm "/etc/systemd/system/$service"
echo "Service $service successfully removed."
else
echo "The $service service did not exist."
fi
done
# Reload systemd to remove any residual references
sudo systemctl daemon-reload
disabilitano ed eliminano i servizi videosurveillance.service e host_service.service dal sistema e ricaricano systemd per rimuovere riferimenti residui ai servizi.
Le righe
echo "Clean up photos, videos and database folders..."
sudo rm -rf ./photos/*
sudo rm -rf ./videos/*
sudo rm -f ./data/videosurveillance_db.sqlite
# Remove the backup
echo "Cleaning backups..."
sudo rm ./backup.zip
sudo rm ./db_dump.db
# Removes the log
echo "Cleaning the log..."
sudo rm ./restore_backup.log
cancellano completamente tutti i file nelle cartelle photos/ e videos/, eliminano il database videosurveillance_db.sqlite, rimuovendo tutti i metadati di foto e video. Eliminano il file di backup backup.zip e il dump del database db_dump.db. Cancellano il log restore_backup.log per evitare residui di sessioni precedenti.
Il messaggio finale
echo "File cleanup completed."
echo "Hard cleanup completed. All Docker and system data have been reset."
conferma che tutti i file, container, immagini e servizi sono stati rimossi.
⚠️ Attenzione prima di eseguire questo script!
- Tutti i dati andranno persi. Lo script NON crea un backup prima della cancellazione.
- Docker verrà completamente svuotato, quindi sarà necessario ricostruire tutto con setup_videosurveillance.sh.
- Assicurati di voler davvero eseguire la pulizia totale prima di confermare con
y
.
Utilizzo
Rendere lo script eseguibile (se non lo è già) dando il comando:
chmod +x hard_clean_docker.sh
Lanciare lo script col comando:
./hard_clean_docker.sh
📌 Quando usare hard_clean_docker.sh?
✅ Quando si vuole eliminare completamente il sistema e ripartire da zero.
✅ Se Docker o i servizi sono corrotti e servono reinstallazione e configurazione da capo.
✅ Dopo aver testato il sistema e si vuole eseguire una pulizia completa prima di una nuova installazione.
📌 Quando NON usare hard_clean_docker.sh?
❌ Se si vogliono mantenere dati e configurazioni (in questo caso usare soft_clean_docker.sh).
❌ Se si vuole solo cancellare foto/video, esiste lo script clean_data.sh per questo scopo.
❌ Se non si è sicuri di voler resettare completamente Docker e il sistema.
🔄 Alternativa: Soft Cleanup
Se non vuoi cancellare tutto, puoi usare soft_clean_docker.sh, che esegue una pulizia più leggera senza rimuovere immagini e servizi.
Lo script poweroff_raspberry.sh
Lo script poweroff_raspberry.sh è un semplice ma importante script di shell che permette di arrestare in modo sicuro e ordinato la Raspberry Pi, garantendo la corretta chiusura di tutti i servizi attivi relativi al sistema di videosorveglianza prima dello spegnimento. Questo evita corruzioni di dati e problemi legati a uno spegnimento improvviso, proteggendo il sistema e il database.
#!/bin/bash
echo "Stopping videosurveillance service..."
sudo systemctl stop videosurveillance.service
echo "Stopping host_service service..."
sudo systemctl stop host_service.service
echo "Shutting down Raspberry PI..."
sudo shutdown -h now
Le righe
#!/bin/bash
echo "Stopping videosurveillance service..."
sudo systemctl stop videosurveillance.service
echo "Stopping host_service service..."
sudo systemctl stop host_service.service
specificano che lo script deve essere eseguito utilizzando la shell Bash, fermano il servizio videosurveillance.service, che gestisce l’acquisizione e il monitoraggio delle immagini e dei video. Questo garantisce che tutte le operazioni in corso vengano correttamente concluse prima dello spegnimento. Fermano il servizio Flask che gestisce alcune API di sistema, come il riavvio remoto del servizio. Questo garantisce che il backend non riceva richieste pendenti durante il processo di spegnimento.
Le righe
echo "Shutting down Raspberry PI..."
sudo shutdown -h now
avvisano l’utente dell’imminente spegnimento della Raspberry, eseguono il comando shutdown -h now, che:
- arresta tutti i processi in esecuzione
- spegne ordinatamente il sistema operativo
- disabilita tutte le connessioni attive e protegge il filesystem
Il parametro -h (halt) indica che il sistema deve fermarsi completamente.
💡 Quando usare poweroff_raspberry.sh
✅ Quando si vuole arrestare in sicurezza la Raspberry Pi senza rischiare corruzione dei dati.
✅ Prima di scollegare l’alimentazione fisica per evitare danni al filesystem o al database.
✅ Quando il sistema è in uno stato di funzionamento normale ma è necessario eseguire uno spegnimento ordinato.
⚠️ Attenzione
- lo script richiede privilegi di amministratore (sudo), quindi deve essere eseguito con un utente che abbia questi permessi
- non spegnere forzatamente la Raspberry Pi scollegando l’alimentazione senza usare questo script
Utilizzo
Se lo script non è già eseguibile, renderlo tale dando il comando:
chmod +x poweroff_raspberry.sh
Eseguire lo script dando il comando:
./poweroff_raspberry.sh
🚨 Cosa fa lo script in sintesi:
- ferma il servizio principale di videosorveglianza.
- ferma il servizio Flask che gestisce le API di sistema.
- arresta in modo sicuro la Raspberry Pi, evitando la corruzione del database e dei file critici.
🔄 Differenze con uno spegnimento forzato
Metodo | Sicurezza dati | Spegnimento servizi | Protezione del filesystem |
---|---|---|---|
poweroff_raspberry.sh | ✅ Sicura | ✅ Completato | ✅ Protetto |
Staccare la corrente | ❌ Non sicuro | ❌ Non completato | ❌ Possibile corruzione |
Questo script è essenziale per mantenere stabile e affidabile il sistema di videosorveglianza.
Lo script reset_admin_db.sh
Questo script serve a resettare la tabella utenti e reimpostare le credenziali dell’utente amministratore del sistema di videosorveglianza salvate nel database SQLite utilizzato dal sistema di videosorveglianza. È particolarmente utile in situazioni in cui si perde l’accesso all’account amministrativo, riportando lo stato della tabella utenti a quello iniziale (e quindi cancellando tutti gli utenti tranne l’amministratore che ha id = 1).
#!/bin/bash
# Path to SQLite file
DB_PATH="/home/pi/videosurveillance/data/videosurveillance_db.sqlite"
# Check if the file exists
if [ ! -f "$DB_PATH" ]; then
echo "Error: Database does not exist at the specified path: $DB_PATH"
exit 1
fi
# SQL commands to execute
SQL_COMMANDS="
DELETE FROM users WHERE id != 1;
UPDATE users
SET username = 'admin',
password = '$pbkdf2-sha256\$29000$RwiBcO4dY4wRAoAQwjjnnA\$9iWG5vcAalsIN8tfWNHZj/nJausfezdDa16YzeabKcs',
is_admin = 1
WHERE id = 1;
"
# Execute SQL commands on the database
echo "Making changes to the database..."
sudo sqlite3 "$DB_PATH" "$SQL_COMMANDS"
# Check if the commands were executed correctly
if [ $? -eq 0 ]; then
echo "Changes applied successfully."
else
echo "Error applying changes."
exit 1
fi
# Check the contents of the database
echo "Verifying users in the database..."
sudo sqlite3 "$DB_PATH" "SELECT * FROM users;"
La riga
DB_PATH="/home/pi/videosurveillance/data/videosurveillance_db.sqlite"
definisce la posizione del file SQLite che contiene i dati del sistema.
Il blocco
if [ ! -f "$DB_PATH" ]; then
echo "Error: Database does not exist at the specified path: $DB_PATH"
exit 1
fi
controlla l’esistenza del database. Se il file non esiste nel percorso specificato, lo script interrompe l’esecuzione con un messaggio di errore.
Il blocco
SQL_COMMANDS="
DELETE FROM users WHERE id != 1;
UPDATE users
SET username = 'admin',
password = '$pbkdf2-sha256\$29000$RwiBcO4dY4wRAoAQwjjnnA\$9iWG5vcAalsIN8tfWNHZj/nJausfezdDa16YzeabKcs',
is_admin = 1
WHERE id = 1;
"
definisce due comandi SQL:
- DELETE FROM users WHERE id != 1;
elimina tutti gli utenti tranne quello con id = 1. - UPDATE users … WHERE id = 1;
aggiorna l’utente con id = 1, reimpostando il nome utente su admin e la password su una predefinita (in formato hash PBKDF2 per garantire la sicurezza). La password di default è 123456.
I comandi SQL vengono eseguiti sul file del database tramite il comando sqlite3:
sudo sqlite3 "$DB_PATH" "$SQL_COMMANDS"
Se l’esecuzione dei comandi SQL ha successo ($? -eq 0), viene mostrato un messaggio di conferma. In caso contrario, lo script termina con un errore:
if [ $? -eq 0 ]; then
echo "Changes applied successfully."
else
echo "Error applying changes."
exit 1
fi
Il blocco successivo effettua un controllo finale del contenuto del database:
echo "Verifying users in the database..."
sudo sqlite3 "$DB_PATH" "SELECT * FROM users;"
Quando e perché usarlo 🔑
- Reset delle credenziali: se l’amministratore perde l’accesso al sistema o dimentica le credenziali.
- Pulizia del database: se si vuole eliminare rapidamente tutti gli utenti tranne quello amministrativo.
- Debug o manutenzione:per verificare la consistenza del database e ripristinare l’utente admin predefinito.
Note di Sicurezza 🔒
- La password impostata nello script è predefinita e statica (123456), quindi è consigliabile cambiarla subito dopo il reset tramite l’interfaccia del sistema.
- Lo script richiede privilegi sudo perché accede al file del database SQLite e lo modifica direttamente.
- Il comando SELECT * FROM users; potrebbe rivelare dati sensibili in console; meglio eseguire lo script solo su terminali sicuri.
Utilizzo
La riga di comando per eseguire lo script r
eset_admin_db.sh è:
sudo ./reset_admin_db.sh
Nota: è importante usare sudo poiché lo script richiede privilegi elevati per modificare il database.
Lo script restore_backup.sh
Lo script restore_backup.sh ha lo scopo di ripristinare i dati di backup (foto, video e database) del sistema di videosorveglianza. Durante il processo:
- Arresta il servizio videosurveillance.
- Verifica l’esistenza di un file di backup (backup.zip) nella cartella di progetto videosurveillance.
- Esegue una copia temporanea del database corrente (se presente).
- Cancella i dati esistenti (foto, video e database) e li sostituisce con quelli presenti nel backup.
- Ripristina le cartelle principali e assegna i permessi corretti ai file.
- Riavvia il servizio videosurveillance al termine del processo.
#!/bin/bash
# Name of the script: restore_backup.sh
# Purpose: Restore backup of video surveillance system
# Variables
PROJECT_DIR=$(pwd) # The directory where the script is executed
BACKUP_FILE="$PROJECT_DIR/backup.zip"
TMP_DIR="$PROJECT_DIR/restore_tmp"
LOG_FILE="$PROJECT_DIR/restore_backup.log"
# Log function
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
# Initial confirmation
echo "You are about to restore a backup. Your current data will be replaced."
read -p "Do you want to proceed? [y/N]: " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo "Restore canceled."
exit 1
fi
# Checking for the existence of the backup.zip file
if [[ ! -f "$BACKUP_FILE" ]]; then
log "Error: The backup file $BACKUP_FILE does not exist."
exit 1
fi
# Stop the service
log "Service shutdown videosurveillance.service..."
sudo systemctl stop videosurveillance.service || {
log "Error stopping service."
exit 1
}
# Temporary backup of the current database
log "Creating a temporary backup of the current database..."
if [[ -f "$PROJECT_DIR/data/videosurveillance_db.sqlite" ]]; then
cp "$PROJECT_DIR/data/videosurveillance_db.sqlite" "$PROJECT_DIR/data/videosurveillance_db.sqlite.bak"
log "Backup of existing database completed."
else
log "No current database found, skipping backup."
fi
# Deleting existing folders
log "Erasing the contents of the photos, videos and data folders..."
rm -rf "$PROJECT_DIR/photos/*" "$PROJECT_DIR/videos/*" "$PROJECT_DIR/data/*" || {
log "Error deleting folders."
exit 1
}
# Unzip to a temporary folder
log "Unzip backup file to temporary folder $TMP_DIR..."
mkdir -p "$TMP_DIR"
unzip -q "$BACKUP_FILE" -d "$TMP_DIR" || {
log "Error while unzipping backup file."
rm -rf "$TMP_DIR"
exit 1
}
# Copying content to root folders
log "Copying the restored content to the main folders..."
cp -r "$TMP_DIR/photos/"* "$PROJECT_DIR/photos/"
cp -r "$TMP_DIR/videos/"* "$PROJECT_DIR/videos/"
cp -r "$TMP_DIR/data/"* "$PROJECT_DIR/data/"
# Assigning root permissions
log "Assigning root owner to restored files..."
sudo chown -R root:root "$PROJECT_DIR/photos" "$PROJECT_DIR/videos" "$PROJECT_DIR/data" || {
log "Error assigning permissions."
exit 1
}
# Cleaning the temporary folder
log "Cleaning the temporary folder $TMP_DIR..."
rm -rf "$TMP_DIR"
# Restarting the service
log "Restarting the service videosurveillance.service..."
sudo systemctl start videosurveillance.service || {
log "Error starting service."
exit 1
}
log "Restore completed successfully!"
exit 0
Inizialmente vengono inizializzate le variabili:
PROJECT_DIR=$(pwd)
BACKUP_FILE="$PROJECT_DIR/backup.zip"
TMP_DIR="$PROJECT_DIR/restore_tmp"
LOG_FILE="$PROJECT_DIR/restore_backup.log"
dove
- PROJECT_DIR: la directory di lavoro corrente.
- BACKUP_FILE: percorso del file di backup (backup.zip).
- TMP_DIR: directory temporanea per l’estrazione del backup.
- LOG_FILE: file di log per registrare gli eventi dello script.
La funzione
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}
registra messaggi nel file di log con timestamp.
L’utente deve confermare per avviare il ripristino. Se non conferma (y/Y), lo script si interrompe:
echo "You are about to restore a backup. Your current data will be replaced."
read -p "Do you want to proceed? [y/N]: " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
echo "Restore canceled."
exit 1
fi
Se il file backup.zip non esiste, lo script si interrompe con un messaggio di errore:
if [[ ! -f "$BACKUP_FILE" ]]; then
log "Error: The backup file $BACKUP_FILE does not exist."
exit 1
fi
Arresta il servizio videosurveillance. Se l’arresto fallisce, lo script si interrompe:
log "Service shutdown videosurveillance.service..."
sudo systemctl stop videosurveillance.service || {
log "Error stopping service."
exit 1
}
Esegue un backup temporaneo del database corrente (se esiste):
if [[ -f "$PROJECT_DIR/data/videosurveillance_db.sqlite" ]]; then
cp "$PROJECT_DIR/data/videosurveillance_db.sqlite" "$PROJECT_DIR/data/videosurveillance_db.sqlite.bak"
log "Backup of existing database completed."
else
log "No current database found, skipping backup."
fi
Vengono poi eliminati tutti i file nelle cartelle photos, videos e data:
log "Erasing the contents of the photos, videos and data folders..."
rm -rf "$PROJECT_DIR/photos/*" "$PROJECT_DIR/videos/*" "$PROJECT_DIR/data/*" || {
log "Error deleting folders."
exit 1
}
Viene poi creata una cartella temporanea (restore_tmp) e vi viene estratto il contenuto del file backup.zip:
log "Unzip backup file to temporary folder $TMP_DIR..."
mkdir -p "$TMP_DIR"
unzip -q "$BACKUP_FILE" -d "$TMP_DIR" || {
log "Error while unzipping backup file."
rm -rf "$TMP_DIR"
exit 1
}
Lo script poi copia i contenuti ripristinati nelle cartelle principali del progetto (photos, videos, data):
log "Copying the restored content to the main folders..."
cp -r "$TMP_DIR/photos/"* "$PROJECT_DIR/photos/"
cp -r "$TMP_DIR/videos/"* "$PROJECT_DIR/videos/"
cp -r "$TMP_DIR/data/"* "$PROJECT_DIR/data/"
e assegna l’owner root e i permessi corretti ai file ripristinati:
log "Assigning root owner to restored files..."
sudo chown -R root:root "$PROJECT_DIR/photos" "$PROJECT_DIR/videos" "$PROJECT_DIR/data" || {
log "Error assigning permissions."
exit 1
}
Viene poi eliminata la cartella temporanea:
log "Cleaning the temporary folder $TMP_DIR..."
rm -rf "$TMP_DIR"
lo script riavvia il servizio videosurveillance. Se il riavvio fallisce, mostra un errore e si interrompe.
log "Restarting the service videosurveillance.service..."
sudo systemctl start videosurveillance.service || {
log "Error starting service."
exit 1
}
Lo script si conclude con un messaggio di successo e termina:
log "Restore completed successfully!"
exit 0
Utilizzo
Per lanciare lo script è sufficiente dare il comando:
sudo ./restore_backup.sh
⚠️Importante! Ripristinare un backup specifico scaricato in precedenza
Quando esegui lo script restore_backup.sh, viene ripristinato automaticamente il file backup.zip presente nella cartella principale del progetto (/home/pi/videosurveillance). Tuttavia, se hai scaricato un backup precedente e vuoi ripristinare proprio quello, segui questi passaggi:
Trasferisci il file backup.zip nella Raspberry Pi
Il procedimento è lo stesso che hai già utilizzato per trasferire il file videosurveillance.zip durante l’installazione iniziale del sistema. Puoi scegliere il metodo che preferisci:
Da Linux/macOS (consigliato): usa rsync per un trasferimento sicuro e affidabile. Ecco il comando:
rsync -avzP /path/to/your/backup.zip [email protected]:/home/pi/videosurveillance/
Da Windows: segui la stessa procedura già spiegata con WinSCP per caricare backup.zip nella directory /home/pi/videosurveillance/.
Sia rsync che WinSCP sovrascrivono il file backup.zip se ne trovano già uno nella directory di destinazione (/home/pi/videosurveillance/). Questo significa che il nuovo file backup.zip sostituirà quello esistente.
Se vuoi evitare di sovrascrivere accidentalmente un backup esistente, puoi rinominare il file di destinazione prima del trasferimento, ad esempio:
mv /home/pi/videosurveillance/backup.zip /home/pi/videosurveillance/backup_old.zip
Una volta completato il trasferimento, verifica la presenza del file backup.zip nella cartella di progetto (/home/pi/videosurveillance/):
ls -la /home/pi/videosurveillance/backup.zip
Se lo vedi nella lista, sei pronto per il ripristino!
Esegui lo script restore_backup.sh:
./restore_backup.sh
Lo script cancellerà i dati attuali e ripristinerà il backup dal file backup.zip appena trasferito.
Lo script soft_clean_docker.sh
Lo script soft_clean_docker.sh esegue una pulizia leggera delle risorse Docker non più utilizzate, senza eliminare contenitori o volumi attivi. È utile per mantenere il sistema Docker pulito e recuperare spazio su disco senza interrompere servizi o container in esecuzione.
#!/bin/bash
read -p "Do you really want to do a soft clean? (y/n): " confirmation
if [[ $confirmation != "y" ]]; then
echo "Cleaning cancelled."
exit 0
fi
echo "Running the Docker soft clean..."
# Initial state
echo "### Initial state of Docker resources ###"
docker system df
# Delete stopped containers
docker container prune -f
# Delete images not associated with active containers
docker image prune -f
# Delete unused networks
docker network prune -f
# Delete unused volumes
docker volume prune -f
# Final state
echo "### Final state of Docker resources ###"
docker system df
echo "Soft cleanup completed. In-use resources preserved."
Il blocco
read -p "Do you really want to do a soft clean? (y/n): " confirmation
if [[ $confirmation != "y" ]]; then
echo "Cleaning cancelled."
exit 0
fi
Chiede conferma all’utente prima di procedere. Se l’utente non risponde y
, lo script si interrompe senza fare nulla.
Le righe
echo "### Initial state of Docker resources ###"
docker system df
mostrano lo stato attuale delle risorse Docker (container, immagini, volumi e reti).
La riga
docker container prune -f
elimina tutti i container che non sono in esecuzione.
Le righe
docker image prune -f
docker network prune -f
docker volume prune -f
eliminano rispettivamente le immagini inutilizzate, le network non in uso e i volumi inutilizzati mantenendo solo le immagini collegate a container attivi.
Lo script, poi, mostra lo stato delle risorse Docker dopo la pulizia per confrontarlo con quello iniziale:
echo "### Final state of Docker resources ###"
docker system df
Lo script termina con la conferma che la pulizia è stata completata con successo, specificando che le risorse in uso sono state mantenute:
echo "Soft cleanup completed. In-use resources preserved."
Utilizzo
Per lanciare lo script è sufficiente dare il comando:
sudo ./soft_clean_docker.sh
Lo script install_docker.sh
Questo script prepara la Raspberry Pi per eseguire correttamente il sistema di videosorveglianza, effettuando:
- Modifica della configurazione dello swap per aumentare la memoria virtuale disponibile, utile durante la compilazione di pacchetti pesanti come numpy.
- Installazione di Docker e Docker Compose, essenziali per eseguire i container del backend e del frontend del progetto.
- Aggiunta dell’utente corrente al gruppo Docker, in modo da evitare l’uso di sudo per eseguire comandi Docker.
#!/bin/bash
# Install sqlite3 and python3-flask
sudo apt update && sudo apt install -y sqlite3 python3-flask
# Increase swap to 2048 MB (change as needed)
SWAPSIZE=2048
echo "Configuring swap size to $SWAPSIZE MB..."
# Edit the dphys-swapfile configuration file
sudo sed -i "s/^CONF_SWAPSIZE=.*/CONF_SWAPSIZE=$SWAPSIZE/" /etc/dphys-swapfile
# Restart the swap service
sudo systemctl stop dphys-swapfile
sudo systemctl start dphys-swapfile
echo "Swap configuration completed. Current swap status:"
free -h
echo "Checking and installing Docker and Docker Compose if necessary..."
# Check if Docker is installed
if ! [ -x "$(command -v docker)" ]; then
echo "Docker not found. Installing Docker..."
curl -sSL https://get.docker.com | sh
else
echo "Docker is already installed."
docker --version
fi
# Adding the current user to the Docker group (if not already present)
if groups $USER | grep &>/dev/null "\bdocker\b"; then
echo "User is already part of the 'docker' group."
else
echo "Adding user to the 'docker' group..."
sudo usermod -aG docker $USER
echo "You must reboot your system for changes to take effect."
echo "After rebooting, run the 'setup_videosurveillance.sh' script."
fi
# Check if Docker Compose is installed
if ! [ -x "$(command -v docker-compose)" ]; then
echo "Docker Compose not found. Installing Docker Compose..."
sudo apt update && sudo apt install -y docker-compose
else
echo "Docker Compose is already installed."
docker-compose --version
fi
echo "Docker installation and configuration completed."
echo "Please reboot your system and then run the setup script."
Il blocco
# Install sqlite3 and python3-flask
sudo apt update && sudo apt install -y sqlite3 python3-flask
SWAPSIZE=2048
echo "Configuring swap size to $SWAPSIZE MB..."
sudo sed -i "s/^CONF_SWAPSIZE=.*/CONF_SWAPSIZE=$SWAPSIZE/" /etc/dphys-swapfile
sudo systemctl stop dphys-swapfile
sudo systemctl start dphys-swapfile
echo "Swap configuration completed. Current swap status:"
free -h
- Installa sqlite3 e python3-flask
- Aumenta la dimensione dello swap a 2048 MB.
- Riavvia il servizio dphys-swapfile per applicare la nuova configurazione.
- Verifica lo stato attuale dello swap usando il comando free -h.
Nota: un maggiore swap aiuta a evitare blocchi durante la compilazione di moduli Python complessi.
Il blocco
if ! [ -x "$(command -v docker)" ]; then
echo "Docker not found. Installing Docker..."
curl -sSL https://get.docker.com | sh
else
echo "Docker is already installed."
docker --version
fi
verifica se Docker è già installato. In caso contrario, lo scarica e installa utilizzando lo script ufficiale di Docker.
Il blocco successivo
if groups $USER | grep &>/dev/null "\bdocker\b"; then
echo "User is already part of the 'docker' group."
else
echo "Adding user to the 'docker' group..."
sudo usermod -aG docker $USER
echo "You must reboot your system for changes to take effect."
echo "After rebooting, run the 'setup_videosurveillance.sh' script."
fi
verifica se l’utente appartiene già al gruppo Docker. Se non lo è, aggiunge l’utente al gruppo docker e informa che è necessario un riavvio per applicare la modifica.
Il blocco
if ! [ -x "$(command -v docker-compose)" ]; then
echo "Docker Compose not found. Installing Docker Compose..."
sudo apt update && sudo apt install -y docker-compose
else
echo "Docker Compose is already installed."
docker-compose --version
fi
verifica la presenza di Docker Compose e lo installa se necessario.
Lo script si conclude con alcuni messaggi che comunicano all’utente l’avvenuta installazione e lo invitano a fare un reboot della Raspberry:
echo "Docker installation and configuration completed."
echo "Please reboot your system and then run the setup script."
Utilizzo
Per lanciare lo script esegui il comando
sudo ./install_docker.sh
al termine devi riavviare la Raspberry Pi come indicato alla fine dello script. Una volta riavviato il sistema, esegui lo script setup_videosurveillance.sh per completare l’installazione del progetto.
Lo script setup_videosurveillance.sh
Questo script ha lo scopo di completare la configurazione del progetto Video Surveillance sulla Raspberry Pi dopo che Docker è stato correttamente installato. Si occupa di:
- Installare pacchetti essenziali per compilare moduli Python avanzati.
- Configurare permessi e owner per file e directory del progetto.
- Creare e configurare i container Docker definiti nel docker-compose.yml.
- Installare Flask per il servizio host_service.py.
- Copiare i file di servizio systemd nella directory corretta.
- Abilitare e avviare i servizi necessari (videosurveillance.service e host_service.service).
#!/bin/bash
# These packages are used to compile advanced Python modules, especially when dealing with libraries
# that require C/C++ components.
echo "installing packages used to compile advanced Python modules"
sudo apt install gcc python3-dev libffi-dev build-essential -y
# Set owner and permissions for specific folders
echo "setting owner and permissions for specific folders"
sudo chown -R root:root data photos videos __pycache__
sudo chmod -R 755 photos videos data __pycache__
# Specific permissions for files inside 'data', 'photos', and 'videos'
echo "setting permissions for files inside data, photos, and videos folders"
find ./data -type f -exec sudo chmod 644 {} \;
find ./photos -type f -exec sudo chmod 644 {} \;
find ./videos -type f -exec sudo chmod 644 {} \;
# Ensure that log and backup files are owned by pi and have appropriate permissions
find ./ -name "*.log" -exec sudo chown pi:pi {} \; -exec sudo chmod 644 {} \;
find ./ -name "backup.zip" -exec sudo chown pi:pi {} \; -exec sudo chmod 644 {} \;
# Name of services
VIDEOSURVEILLANCE_SERVICE="videosurveillance.service"
HOST_SERVICE="host_service.service"
# Service file path
VIDEOSURVEILLANCE_FILE="./$VIDEOSURVEILLANCE_SERVICE"
HOST_SERVICE_FILE="./$HOST_SERVICE"
# systemd path to service files
SYSTEMD_PATH="/etc/systemd/system"
# Cleaning and disposal of unused containers
echo "Cleaning of existing containers..."
docker ps -a --filter "name=videosurveillance" --format "{{.ID}}" | xargs -r docker rm -f
echo "Existing containers removed."
# Creation of containers
echo "Creating containers defined in docker-compose.yml..."
docker-compose up --no-start
echo "Containers successfully created."
echo "Installing Flask and necessary dependencies for the host_service.py service..."
pip install Flask
# Copy of video surveillance service file
echo "Copying the service file $VIDEOSURVEILLANCE_SERVICE in $SYSTEMD_PATH..."
if [ -f "$VIDEOSURVEILLANCE_FILE" ]; then
sudo cp "$VIDEOSURVEILLANCE_FILE" "$SYSTEMD_PATH/$VIDEOSURVEILLANCE_SERVICE"
echo "Service file $VIDEOSURVEILLANCE_SERVICE copied successfully."
else
echo "Error: The file $VIDEOSURVEILLANCE_FILE does not exist. Aborting."
exit 1
fi
# Copy of the host_service service file
echo "Copying the service file $HOST_SERVICE in $SYSTEMD_PATH..."
if [ -f "$HOST_SERVICE_FILE" ]; then
sudo cp "$HOST_SERVICE_FILE" "$SYSTEMD_PATH/$HOST_SERVICE"
echo "Service file $HOST_SERVICE copied successfully."
else
echo "Error: File $HOST_SERVICE_FILE does not exist. Aborting."
exit 1
fi
# Reload systemd services and enable services
echo "I reload systemd, enable and start services..."
sudo systemctl daemon-reload
sudo systemctl enable "$VIDEOSURVEILLANCE_SERVICE"
sudo systemctl enable "$HOST_SERVICE"
sudo systemctl start "$VIDEOSURVEILLANCE_SERVICE"
sudo systemctl start "$HOST_SERVICE"
# Final notice
echo "Setup complete! The following services have been configured and started:"
echo "1. $VIDEOSURVEILLANCE_SERVICE"
echo "2. $HOST_SERVICE"
echo ""
echo "You can manage services with the following commands:"
echo "sudo systemctl enable/disable/start/stop/restart/status videosurveillance.service"
echo "sudo systemctl enable/disable/start/stop/restart/status host_service.service"
Il comando
echo "installing packages used to compile advanced Python modules"
sudo apt install gcc python3-dev libffi-dev build-essential -y
installa i pacchetti necessari per compilare moduli Python che richiedono componenti in C/C++ (ad esempio numpy e cryptography).
Questo blocco
sudo chown -R root:root data photos videos __pycache__
sudo chmod -R 755 photos videos data __pycache__
find ./data -type f -exec sudo chmod 644 {} \;
find ./photos -type f -exec sudo chmod 644 {} \;
find ./videos -type f -exec sudo chmod 644 {} \;
find ./ -name "*.log" -exec sudo chown pi:pi {} \; -exec sudo chmod 644 {} \;
find ./ -name "backup.zip" -exec sudo chown pi:pi {} \; -exec sudo chmod 644 {} \;
Imposta root:root come owner per le directory data, photos, videos, e __pycache__ e assegna permessi 755 alle directory e 644 ai file all’interno di data, photos e videos. Inoltre garantisce che i file .log e backup.zip siano di proprietà dell’utente pi e abbiano permessi 644.
Il blocco seguente
docker ps -a --filter "name=videosurveillance" --format "{{.ID}}" | xargs -r docker rm -f
docker-compose up --no-start
rimuove eventuali container esistenti relativi al progetto e crea nuovi container definiti nel file docker-compose.yml senza avviarli.
Viene poi installato Flask, necessario per il funzionamento del servizio host_service.py:
pip install Flask
Vengono poi copiati i file di servizio videosurveillance.service e host_service.service nella directory /etc/systemd/system, rendendoli gestibili tramite systemd:
VIDEOSURVEILLANCE_SERVICE="videosurveillance.service"
HOST_SERVICE="host_service.service"
SYSTEMD_PATH="/etc/systemd/system"
sudo cp "$VIDEOSURVEILLANCE_FILE" "$SYSTEMD_PATH/$VIDEOSURVEILLANCE_SERVICE"
sudo cp "$HOST_SERVICE_FILE" "$SYSTEMD_PATH/$HOST_SERVICE"
Il blocco seguente
sudo systemctl daemon-reload
sudo systemctl enable "$VIDEOSURVEILLANCE_SERVICE"
sudo systemctl enable "$HOST_SERVICE"
sudo systemctl start "$VIDEOSURVEILLANCE_SERVICE"
sudo systemctl start "$HOST_SERVICE"
ricarica i servizi systemd per registrare i nuovi file di configurazione. Abilita e avvia i servizi videosurveillance.service e host_service.service.
Lo script si conclude con un messaggio di conferma che avvisa l’utente che la configurazione è completa e i servizi sono attivi:
echo "Setup complete! The following services have been configured and started:"
Utilizzo
💻 Prerequisiti
- Docker e Docker Compose devono essere già installati. Se non lo sono, utilizza lo script install_docker.sh per prepararli.
- Assicurati di essere nella directory del progetto (/home/pi/videosurveillance) prima di lanciare lo script.
⚙️ Esecuzione dello script
Per eseguire lo script, usa il seguente comando nella terminale:
sudo ./setup_videosurveillance.sh
🔑 Nota: lo script richiede permessi di amministratore (sudo) poiché modifica file di sistema e avvia servizi systemd.
Lo script install_wireguard.sh
Lo script install_wireguard.sh automatizza il processo di installazione e configurazione di WireGuard su Raspberry Pi. Permette di configurare un server VPN, generare automaticamente la configurazione per il client e fornire un QR code per una rapida configurazione su smartphone.
Vediamo ora il suo funzionamento nel dettaglio.
Introduzione e messaggio iniziale
#!/bin/bash
echo "Installing and configuring WireGuard..."
Definisce lo script come eseguibile bash (#!/bin/bash). Stampa un messaggio per indicare che l’installazione è in corso.
Ottenere l’IP pubblico del server
SERVER_IP=$(curl -s -4 https://ifconfig.me)
Utilizza curl per ottenere l’IP pubblico della connessione, necessario per la configurazione del client. Il comando ifconfig.me restituisce l’IP attuale del Raspberry visto da Internet.
📌 Nota: se l’utente ha un IP dinamico, dovrà configurare un DDNS per aggiornarlo automaticamente.
Generazione delle chiavi di sicurezza
SERVER_PORT=51820
SERVER_PRIVKEY=$(wg genkey)
SERVER_PUBKEY=$(echo "$SERVER_PRIVKEY" | wg pubkey)
CLIENT_PRIVKEY=$(wg genkey)
CLIENT_PUBKEY=$(echo "$CLIENT_PRIVKEY" | wg pubkey)
CLIENT_IP="10.0.0.2/32"
Genera automaticamente le chiavi di crittografia per il server e il client.
Definisce la porta di ascolto 51820, che dovrà essere aperta nel router.
Assegna al client l’IP 10.0.0.2/32 nella VPN.
📌 Nota: ogni client avrà un IP univoco all’interno della rete VPN.
Installazione di WireGuard (se non è già presente)
if ! command -v wg &> /dev/null; then
sudo apt update
sudo apt install -y wireguard qrencode
else
echo "WireGuard is already installed."
fi
Controlla se WireGuard è già installato (command -v wg).
Se non è installato, lo scarica e installa insieme a qrencode, necessario per generare il QR code della configurazione client.
📌 Nota: se WireGuard è già presente, lo script lo segnala e continua senza reinstallarlo.
Creazione della configurazione del server
echo "Configuring the server..."
cat <<EOF | sudo tee /etc/wireguard/wg0.conf
[Interface]
Address = 10.0.0.1/24
PrivateKey = $SERVER_PRIVKEY
ListenPort = $SERVER_PORT
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -A FORWARD -o wg0 -j ACCEPT
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -D FORWARD -o wg0 -j ACCEPT
[Peer]
PublicKey = $CLIENT_PUBKEY
AllowedIPs = $CLIENT_IP
EOF
Crea il file di configurazione /etc/wireguard/wg0.conf per il server.
Imposta l’indirizzo della VPN su 10.0.0.1/24.
Definisce la porta 51820 per accettare connessioni.
Aggiunge regole iptables per abilitare il forwarding del traffico della VPN.Configura il client con il suo PublicKey e l’IP assegnato.
📌 Nota: le regole PostUp e PostDown garantiscono che il traffico possa fluire attraverso la VPN.
Creazione della configurazione del client
echo "Configuring the client..."
cat <<EOF > client.conf
[Interface]
PrivateKey = $CLIENT_PRIVKEY
Address = $CLIENT_IP
DNS = 8.8.8.8
[Peer]
PublicKey = $SERVER_PUBKEY
Endpoint = $SERVER_IP:$SERVER_PORT
AllowedIPs = 192.168.1.0/24
PersistentKeepalive = 25
EOF
Genera il file di configurazione client.conf per il client.
Imposta l’IP privato 10.0.0.2/32.
Definisce il DNS 8.8.8.8 (Google) per la risoluzione dei nomi.
Specifica l’IP pubblico del server e la porta 51820 per connettersi.
Configura AllowedIPs = 192.168.1.0/24 per accedere alla rete interna.
Aggiunge PersistentKeepalive = 25 per mantenere attiva la connessione.
📌 Nota: il file client.conf può essere importato direttamente su un PC o trasformato in QR code per smartphone.
Avvio automatico del servizio WireGuard
echo "Starting WireGuard..."
sudo systemctl enable wg-quick@wg0
sudo systemctl start wg-quick@wg0
Abilita WireGuard all’avvio del sistema (enable). Avvia immediatamente il servizio (start).
📌 Nota: se la Raspberry viene riavviata, WireGuard partirà automaticamente.
Generazione del QR Code per la configurazione del client
echo "Scan this QR code with the WireGuard app:"
qrencode -t UTF8 < client.conf
Genera un QR code basato sul contenuto di client.conf.
Il codice QR può essere scansionato direttamente con l’app WireGuard su Android e iOS.
📌 Nota: questo evita di dover copiare manualmente la configurazione sullo smartphone.
Visualizzazione della configurazione client in chiaro
echo "Client configuration (for PC users):"
cat client.conf
Stampa a schermo il contenuto di client.conf, utile per chi vuole copiare manualmente la configurazione su PC Windows/Linux/macOS.
📌 Nota: gli utenti desktop dovranno importare manualmente questo file nel client WireGuard.
Riavvio della rete per applicare le modifiche
sudo systemctl restart networking
Riavvia il servizio di rete per applicare le nuove configurazioni di routing.
📌 Nota: un riavvio della Raspberry è comunque raccomandato.
Messaggio di conferma e istruzioni finali
echo "WireGuard has been successfully configured!"
echo "To manually restart WireGuard: sudo systemctl restart wg-quick@wg0"
echo "Please reboot your system..."
Conferma che WireGuard è stato installato con successo.
Mostra il comando per riavviare manualmente il servizio in caso di problemi.
Suggerisce un riavvio del sistema per garantire il corretto funzionamento.
Nota sulla porta per WireGuard e configurazione del router
Per poter accedere da remoto alla tua Raspberry Pi attraverso WireGuard, devi aprire una porta nel router e reindirizzarla alla Raspberry.
Di default, WireGuard utilizza la porta UDP 51820, ma non tutti i router la accettano.
🎯 Come scegliere la porta giusta?
- Controlla il tuo router: se provi a configurare il port forwarding e ricevi un errore che indica che la porta è fuori intervallo, scegli un’altra porta all’interno di 32768-40959.
- Evita porte già usate da altri servizi: non scegliere porte riservate a VPN commerciali, server di gioco o VoIP.
- Mantieni UDP: WireGuard funziona solo con il protocollo UDP, quindi non provare con TCP.
Nello script sostituisci la porta 51820 della riga
SERVER_PORT=51820
con la porta da te scelta.
📢 Comandi utili per la gestione dei servizi
Dopo l’esecuzione dello script, puoi gestire i servizi con questi comandi:
Verificare lo stato di un servizio:
sudo systemctl status videosurveillance.service
Avviare un servizio:
sudo systemctl start videosurveillance.service
Riavviare un servizio:
sudo systemctl restart videosurveillance.service
Arrestare un servizio:
sudo systemctl stop videosurveillance.service
Le stesse operazioni sono possibili per il servizio host_service.service.
Il file di configurazione config.ini
Il file di configurazione config.ini raccoglie le configurazioni principali dell’applicazione, in gran parte modificabili tramite la pagina web di configurazione presente nel frontend ed accessibile al solo amministratore. Il file si presenta così:
[general]
init_delay = 15
process_interval = 10
max_photos = 500
batch_size = 50
max_videos = 50
[paths]
photos_dir = /app/photos/motion
videos_dir = /app/videos/motion
batch_upload_api_url = http://localhost:8000/upload_batch/
upload_video_api_url = http://localhost:8000/upload_video/
notify_api_url = http://localhost:8000/notify
photo_directory = photos
thumbnail_directory = photos/thumbnails
video_directory = videos
video_thumbnail_directory = videos/thumbnails
[security]
secret_key = mydefaultsecretkey
[notification]
pushover_enabled = false
pushover_token = 111111111111111111111111111111111
pushover_user_key = 22222222222222222222222222
telegram_enabled = false
telegram_token = 333333333333333333333
chatid_telegram = 444444444444
gmail_enabled = false
smtp_server = smtp.gmail.com
smtp_port = 587
sender_email = [email protected]
app_password = 55555555555555555
receiver_email = [email protected]
La sezione “general”
Questa sezione, modificabile dalla pagina di configurazione, contiene i parametri di configurazione che regolano il comportamento generale dello script di monitoraggio.
init_delay: imposta l’intervallo di tempo (in secondi) tra una scansione della cartella e l’altra. Serve a evitare di elaborare file incompleti se Motion sta ancora avviandosi.
process_interval: imposta l’intervallo di tempo (in secondi) tra una scansione della cartella e l’altra. Lo script attende process_interval secondi prima di controllare di nuovo le cartelle di Motion.
max_photos: definisce il numero massimo di foto che possono essere conservate nel sistema. Se il numero di foto supera max_photos, le foto più vecchie vengono eliminate automaticamente.
batch_size:imposta il numero massimo di file da caricare contemporaneamente in un batch. Serve a ottimizzare il caricamento senza sovraccaricare il sistema o il server.
max_videos: definisce il numero massimo di video che possono essere conservati nel sistema. Se il numero di video supera max_videos, i video più vecchi vengono eliminati automaticamente.
La sezione “paths”
Questa sezione definisce i percorsi delle cartelle dove vengono salvati foto e video, oltre agli URL delle API per l’upload e le notifiche. Tale sezione NON è configurabile dalla pagina di configurazione e NON è configurabile a mano in quanto deve rimanere così com’è.
[paths]
photos_dir = /app/photos/motion
videos_dir = /app/videos/motion
batch_upload_api_url = http://localhost:8000/upload_batch/
upload_video_api_url = http://localhost:8000/upload_video/
notify_api_url = http://localhost:8000/notify
photo_directory = photos
thumbnail_directory = photos/thumbnails
video_directory = videos
video_thumbnail_directory = videos/thumbnails
photos_dir: specifica la cartella principale dove Motion salva le foto catturate. Questa cartella è monitorata dallo script motion_monitor.py per elaborare le immagini.
videos_dir: definisce la cartella in cui vengono salvati i video registrati da Motion. Anche questa cartella è monitorata per verificare la stabilità dei file prima dell’upload.
batch_upload_api_url: contiene l’URL dell’API REST che gestisce l’upload in batch delle foto.
upload_video_api_url: contiene l’URL dell’API REST per il caricamento dei video.
notify_api_url: URL dell’API che invia notifiche agli utenti quando vengono caricati nuovi file.
photo_directory: definisce la cartella principale per l’archiviazione delle foto elaborate.
thumbnail_directory: definisce la cartella in cui vengono salvate le miniature delle foto.
video_directory: definisce la cartella principale per l’archiviazione dei video elaborati.
video_thumbnail_directory: contiene le miniature dei video, che vengono generate estraendo un frame dal video.
La sezione “security”
Questa sezione contiene parametri di sicurezza fondamentali per il funzionamento del sistema. È l’unica sezione che deve essere modificata direttamente editanto il file nella cartella di progetto videosurveillance, per esempio usando il comando:
nano config.ini
secret_key: definisce la chiave segreta utilizzata per generare e verificare i token JWT (JSON Web Token). È fondamentale per l’autenticazione e la protezione delle API. Deve essere cambiata prima di mettere il sistema in produzione, altrimenti un attaccante potrebbe manipolare i token JWT e ottenere accesso non autorizzato.
La sezione “notification”
Questa sezione, modificabile dalla pagina di configurazione, configura le notifiche del sistema, che possono essere inviate tramite Pushover, Telegram o Gmail.
Notifiche via Pushover
pushover_enabled = false
pushover_token = 111111111111111111111111111111111
pushover_user_key = 22222222222222222222222222
pushover_enabled: abilita (true) o disabilita (false) le notifiche tramite Pushover.
pushover_token: è il token dell’applicazione per l’autenticazione con Pushover.
pushover_user_key: è la chiave dell’utente per ricevere le notifiche.
Notifiche via Telegram
telegram_enabled = false
telegram_token = 333333333333333333333
chatid_telegram = 444444444444
telegram_enabled: abilita (true) o disabilita (false) le notifiche su Telegram.
telegram_token: è il token del bot Telegram utilizzato per inviare i messaggi.
chatid_telegram: è l’ID della chat (può essere un singolo utente o un gruppo).
Notifiche via Gmail
gmail_enabled = false
smtp_server = smtp.gmail.com
smtp_port = 587
sender_email = [email protected]
app_password = 55555555555555555
receiver_email = [email protected]
gmail_enabled: abilita (true) o disabilita (false) le notifiche via email (Gmail SMTP).
smtp_server: è il server SMTP di Gmail per inviare email.
smtp_port : 587 è la porta SMTP per l’invio con TLS.
sender_email: è l’indirizzo email del mittente (l’account che invia le notifiche).
app_password: è la password generata per l’app (da creare nelle impostazioni di sicurezza di Google).
receiver_email: è l’indirizzo email del destinatario (chi riceve la notifica).
Per la creazione della applicazione Pushover, il bot Telegram e l’applicazione Gmail ti rimando ai paragrafi specifici.
Gli script Python:il cuore dell’applicazione
Il sistema di videosorveglianza è gestito da quattro script principali scritti in Python. Questi script interagiscono tra loro per monitorare la fotocamera, gestire il database, elaborare foto e video, inviare notifiche e fornire un’interfaccia API tramite FastAPI.
Di seguito, una panoramica di ciascuno di essi:
Lo script main.py: il cuore del backend
Lo script main.py rappresenta il fulcro dell’applicazione, fornendo il backend basato su FastAPI. È responsabile di gestire le API REST, la configurazione del sistema, l’autenticazione utenti e molte altre operazioni.
Funzionalità principali
- Gestione utenti: autenticazione con JWT, creazione di utenti, gestione dell’account admin.
- Gestione file multimediali: upload e download di foto e video, creazione delle miniature.
- API REST: fornisce gli endpoint per interagire con l’interfaccia web.
- Backup automatico: creazione periodica di un file di backup contenente il database e tutti i file multimediali.
- Statistiche di sistema: monitoraggio in tempo reale dell’utilizzo di CPU, RAM, disco e swap.
- Notifiche: invio di notifiche agli utenti tramite Telegram, Pushover ed email quando vengono rilevati nuovi eventi.
- Configurazione dinamica: lettura e aggiornamento dei file di configurazione config.ini e motion.conf.
Nota: lo script avvia automaticamente un processo di backup che viene eseguito periodicamente ogni 10 minuti (impostazione di default ma modificabile) alla riga:
scheduler.add_job(create_backup, "interval", minutes=10)
Lo script svolge le funzionalità seguenti:
Importazione delle librerie
import configparser
from typing import List
import requests
from PIL import Image
from fastapi import FastAPI, File, UploadFile, HTTPException, Body, BackgroundTasks, Depends
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
from database import get_db, Photo, Video, User
import shutil
import os
import smtplib
import subprocess
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
from moviepy.editor import VideoFileClip
from passlib.context import CryptContext
import jwt
from pydantic import BaseModel
import psutil
from apscheduler.schedulers.background import BackgroundScheduler
import zipfile
FastAPI: framework per la gestione delle API REST.
SQLAlchemy: ORM per interagire con il database.
psutil: per ottenere statistiche di sistema (CPU, RAM, disco, swap).
jwt: per la gestione dei token di autenticazione JWT.
PIL, MoviePy: per la gestione delle immagini e dei video.
smtplib, email: per inviare notifiche via email.
requests: per comunicare con altre API (es. Telegram, Pushover).
Configurazione di FastAPI
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
FastAPI è il framework utilizzato per creare le API REST del backend.
CORS Middleware permette di accettare richieste da qualsiasi dominio (necessario per la comunicazione con il frontend).
Lettura della configurazione
config_file_path = '/app/config.ini'
motion_config_path = '/etc/motion/motion.conf'
BACKUP_PATH = "/app/backup.zip"
DB_PATH = "/app/data/videosurveillance_db.sqlite"
config = configparser.ConfigParser()
config.read(config_file_path)
config.ini contiene le impostazioni dell’applicazione.
motion.conf contiene la configurazione del software di motion detection.
Vengono definiti i percorsi per il database e il backup.
Creazione delle directory necessarie
PHOTO_DIRECTORY = config.get('paths', 'photo_directory')
if not os.path.exists(PHOTO_DIRECTORY):
os.makedirs(PHOTO_DIRECTORY)
THUMBNAIL_DIRECTORY = config.get('paths', 'thumbnail_directory')
if not os.path.exists(THUMBNAIL_DIRECTORY):
os.makedirs(THUMBNAIL_DIRECTORY)
VIDEO_DIRECTORY = config.get('paths', 'video_directory')
if not os.path.exists(VIDEO_DIRECTORY):
os.makedirs(VIDEO_DIRECTORY)
VIDEO_THUMBNAIL_DIRECTORY = config.get('paths', 'video_thumbnail_directory')
if not os.path.exists(VIDEO_THUMBNAIL_DIRECTORY):
os.makedirs(VIDEO_THUMBNAIL_DIRECTORY)
Se le cartelle per foto, miniature e video non esistono, vengono create automaticamente.
Autenticazione con JWT
SECRET_KEY = config.get('security', 'secret_key')
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
pwd_context = CryptContext(schemes=["pbkdf2_sha256"], deprecated="auto")
JWT viene utilizzato per gestire l’autenticazione degli utenti.
SECRET_KEY è la chiave segreta per firmare i token.
pwd_context viene usato per l’hashing delle password.
🔐 Cos’è SECRET_KEY e perché è importante cambiarla?
Nel file main.py, la variabile SECRET_KEY è utilizzata per firmare e validare i token JWT (JSON Web Token), che servono per l’autenticazione degli utenti nel sistema di videosorveglianza.
📌 A cosa serve SECRET_KEY?
SECRET_KEY è un elemento fondamentale per la sicurezza del sistema perché:
- Protegge i token JWT: ogni volta che un utente effettua il login, il server genera un token JWT contenente i suoi dati e lo firma con SECRET_KEY. Il client (browser, app) utilizza poi questo token per autenticarsi in tutte le richieste successive.
- Impedisce la falsificazione dei token: se un malintenzionato riuscisse a generare un token JWT valido, potrebbe accedere al sistema come se fosse un utente autorizzato. La chiave segreta impedisce che ciò avvenga.
- Garantisce la sicurezza degli utenti e delle API: senza una chiave sicura, un attaccante potrebbe intercettare o creare token malevoli per accedere al sistema.
⚠️ Perché è pericoloso lasciare SECRET_KEY con il valore di default?
Nel codice è previsto un controllo di sicurezza:
SECRET_KEY = config.get('security', 'secret_key')
if SECRET_KEY == "mydefaultsecretkey":
print("[WARNING] The SECRET_KEY has not been changed! Change it to secure the system.")
Se l’utente non cambia questa chiave, significa che chiunque conosca il codice dell’applicazione può generare token JWT validi e accedere al sistema senza bisogno di una password!
In pratica:
- Se un hacker scopre che il valore di SECRET_KEY è quello di default (mydefaultsecretkey), può creare token JWT validi e autenticarsi come amministratore.
- Questo compromette completamente la sicurezza del sistema, consentendo accessi non autorizzati.
🔄 Come cambiare SECRET_KEY per proteggere il sistema?
Per evitare problemi di sicurezza, l’utente deve modificare SECRET_KEY all’interno del file config.ini.
👉 Aprire il file di configurazione (config.ini) e trovare la sezione [security]:
[security]
secret_key = mydefaultsecretkey
🔑 Sostituire il valore di SECRET_KEY con una stringa lunga e casuale.
Esempio di chiave sicura:
secret_key = wF2@pL6z!9vQmN7s#XyT$3rKdG8B
Puoi generare una chiave sicura usando Python:
import secrets
print(secrets.token_hex(32))
Backup automatico
def create_backup():
print("[INFO] SYSTEM BACKUP")
db_file = "/app/data/videosurveillance_db.sqlite"
db_dump_path = "/app/db_dump.db"
shutil.copyfile(db_file, db_dump_path)
with zipfile.ZipFile(BACKUP_PATH, 'w') as backup_zip:
for folder_name, _, filenames in os.walk(PHOTO_DIRECTORY):
for filename in filenames:
file_path = os.path.join(folder_name, filename)
arcname = os.path.join("photos", filename)
backup_zip.write(file_path, arcname)
for folder_name, _, filenames in os.walk(VIDEO_DIRECTORY):
for filename in filenames:
file_path = os.path.join(folder_name, filename)
arcname = os.path.join("videos", filename)
backup_zip.write(file_path, arcname)
backup_zip.write(db_file, os.path.join("data", os.path.basename(db_file)))
scheduler = BackgroundScheduler()
scheduler.add_job(create_backup, "interval", minutes=10)
scheduler.start()
create_backup()
Viene creato un backup ogni 10 minuti, salvando foto, video e database in backup.zip.
Classe per la richiesta di eliminazione utente
Questa classe definisce la struttura della richiesta JSON per eliminare un utente:
class DeleteUserRequest(BaseModel):
username: str
La classe DeleteUserRequest estende BaseModel di Pydantic. Serve per validare le richieste di eliminazione di un utente, assicurandosi che abbiano almeno il campo username.
Recupero dell’utente autenticato
Questa funzione verifica il token JWT e restituisce l’utente autenticato:
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
try:
print(f"Token received: {token}") # Debug
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
username: str = payload.get("sub")
print(f"Decoded payload: {payload}") # Debug
if username is None:
raise HTTPException(status_code=401, detail="Invalid token")
user = db.query(User).filter(User.username == username).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
return user
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except PyJWTError:
raise HTTPException(status_code=401, detail="Invalid token")
Decodifica il token JWT ricevuto nella richiesta.
Estrae il valore sub che rappresenta lo username dell’utente.
Se il token è scaduto o non valido, viene sollevata un’eccezione HTTP.
Se l’utente esiste nel database, viene restituito.
Creazione di un hash per la password
Questa funzione converte una password in un formato sicuro con un algoritmo di hashing:
def hash_password(password: str) -> str:
return pwd_context.hash(password)
Utilizza Passlib per generare un hash della password. L’hash è usato per proteggere le credenziali memorizzate nel database.
Creazione del token JWT
Questa funzione genera un token di accesso JWT che verrà usato per autenticare le richieste:
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt
data è un dizionario con i dati dell’utente.
Il token ha una scadenza predefinita di 30 minuti.
Usa HS256 come algoritmo di firma con la SECRET_KEY per garantire la sicurezza.
Creazione dell’utente Admin predefinito
@app.on_event("startup")
def create_default_admin():
db: Session = next(get_db())
existing_admin = db.query(User).filter(User.username == "admin").first()
if not existing_admin:
admin = User(
username="admin",
password=hash_password("123456"),
is_admin=True
)
db.add(admin)
db.commit()
print("[INFO] Admin user created with username 'admin' and password '123456'")
else:
print("[INFO] Admin user already present")
All’avvio dell’app, controlla se esiste già un utente admin.Se non esiste, ne crea uno con:
- Username:
"
admin"
- Password:
"
123456"
(hashata) - Ruolo: Admin
Se l’admin esiste già, stampa un messaggio informativo.
Validazione video
Questa funzione verifica che un video sia valido prima di essere salvato:
def is_video_valid(video_path):
try:
with VideoFileClip(video_path) as clip:
duration = clip.duration
if duration <= 0:
print(f"[VALIDATION] Invalid duration (<=0) for video: {video_path}")
return False
print(f"[VALIDATION] Valid duration: {duration} seconds for video {video_path}")
return True
except Exception as e:
print(f"[VALIDATION] Error during video validation {video_path}: {e}")
return False
Apre il file video con MoviePy.
Controlla la durata:
- Se <= 0 secondi, il file è corrotto o non valido (oppure è all’inizio del salvataggio).
- Se tutto va bene, restituisce True.
Se il file non può essere aperto, restituisce False.
Download del backup
Endpoint API per scaricare il file di backup:
@app.get("/download-backup")
def download_backup():
if os.path.exists(BACKUP_PATH):
return FileResponse(BACKUP_PATH, media_type="application/zip", filename="backup.zip")
return {"error": "Backup file not found"}
Controlla se il file backup.zip esiste.
Se esiste, lo restituisce come file scaricabile. Se non esiste, restituisce un messaggio di errore.
Statistiche di sistema
Endpoint per raccogliere informazioni sulle risorse del sistema in tempo reale:
@app.get("/system-stats")
def get_system_stats():
try:
cpu_usage = psutil.cpu_percent(interval=1)
memory = psutil.virtual_memory()
memory_total = memory.total / (1024 ** 2)
memory_used = memory.used / (1024 ** 2)
memory_percent = memory.percent
disk = psutil.disk_usage('/')
disk_total = disk.total / (1024 ** 3)
disk_used = disk.used / (1024 ** 3)
disk_percent = disk.percent
swap = psutil.swap_memory()
swap_total = swap.total / (1024 ** 2)
swap_used = swap.used / (1024 ** 2)
swap_percent = swap.percent
return {
"cpu_usage_percent": cpu_usage,
"memory": {
"total_mb": memory_total,
"used_mb": memory_used,
"percent": memory_percent,
},
"disk": {
"total_gb": disk_total,
"used_gb": disk_used,
"percent": disk_percent,
},
"swap": {
"total_mb": swap_total,
"used_mb": swap_used,
"percent": swap_percent,
}
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error collecting system statistics: {str(e)}")
CPU: percentuale di utilizzo negli ultimi secondi.
RAM: quantità totale, usata e percentuale di utilizzo.
Disco: spazio totale, usato e percentuale di utilizzo.
Swap: memoria virtuale usata e disponibile.
Statistiche su foto e video
Endpoint che raccoglie informazioni sui media archiviati:
@app.get("/media-stats")
def media_stats(db: Session = Depends(get_db)):
import datetime
from pathlib import Path
today = datetime.datetime.now()
last_month = today - datetime.timedelta(days=30)
photos = db.query(Photo).all()
total_photos = len(photos)
recent_photos = [p for p in photos if datetime.datetime.strptime(p.filename.split("-")[1], "%Y%m%d%H%M%S") > last_month]
photo_size_mb = sum(Path(p.filepath).stat().st_size for p in photos) / (1024 * 1024) if photos else 0
return {
"photos": {
"total_count": total_photos,
"last_month_count": len(recent_photos),
"total_size_mb": round(photo_size_mb, 2),
}
}
Conta il numero di foto e video salvati.
Calcola lo spazio occupato in MB.
Conta quanti file sono stati aggiunti nell’ultimo mese.
Login dell’utente
Questo endpoint gestisce il login degli utenti, verificando username e password:
@app.post("/login")
def login(username: str = Body(...), password: str = Body(...), db: Session = Depends(get_db)):
user = db.query(User).filter(User.username == username).first()
if not user or not pwd_context.verify(password, user.password):
raise HTTPException(status_code=400, detail="Invalid username or password")
# Add is_admin to the payload
access_token = create_access_token(data={"sub": user.username, "is_admin": user.is_admin})
return {"access_token": access_token, "token_type": "bearer"}
Verifica se lo username esiste nel database.
Controlla la password usando il modulo Passlib.
Se la password è errata, restituisce un errore 400 – Invalid username or password.
Se i dati sono corretti, genera un token JWT contenente:
- sub: lo username.
- is_admin: valore booleano che indica se l’utente è admin.
Restituisce il token JWT da usare nelle richieste future.
Aggiunta di un nuovo utente
Solo un utente con ruolo admin può creare un nuovo account.
@app.post("/users/add")
def add_user(
username: str = Body(...),
password: str = Body(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Accesso negato: solo l'admin può aggiungere utenti.")
existing_user = db.query(User).filter(User.username == username).first()
if existing_user:
raise HTTPException(status_code=400, detail="Username già in uso.")
hashed_password = pwd_context.hash(password)
new_user = User(username=username, password=hashed_password, is_admin=False)
db.add(new_user)
db.commit()
return {"info": f"Utente '{username}' aggiunto con successo"}
Controlla se l’utente che sta facendo la richiesta è admin.
Se il nome utente esiste già, restituisce errore 400 – Username già in uso.
Se il nome è disponibile:
- Hash della password con Passlib.
- Creazione del nuovo utente nel database.
Conferma con un messaggio di successo.
Modifica di un utente
L’admin può aggiornare le credenziali di un utente:
@app.put("/users/update/{user_id}")
def update_user(
user_id: int,
username: str = Body(None),
password: str = Body(None),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Accesso negato: solo l'admin può modificare utenti.")
user = db.query(User).filter(User.id == user_id).first()
if not user or user.is_admin:
raise HTTPException(status_code=404, detail="Utente non trovato o non modificabile.")
if username:
user.username = username
if password:
user.password = pwd_context.hash(password)
db.commit()
return {"info": f"User with ID {user_id} updated"}
Solo un admin può modificare gli utenti.
Se l’utente non esiste o è un admin, restituisce errore 404 – Utente non modificabile.
Permette di cambiare username e password di un utente normale.
Salva le modifiche nel database.
Cambio password
Permette a un utente di cambiare la propria password:
@app.post("/users/change-password")
def change_password(
current_password: str = Body(...),
new_password: str = Body(...),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
if not pwd_context.verify(current_password, current_user.password):
raise HTTPException(status_code=400, detail="Password attuale errata")
current_user.password = pwd_context.hash(new_password)
db.commit()
return {"info": "Password aggiornata con successo"}
Verifica che la password attuale sia corretta.
Se errata, restituisce errore 400 – Password attuale errata.
Se corretta, aggiorna la password con un nuovo hash.
Conferma con un messaggio di successo.
Aggiornamento delle credenziali Admin
Solo l’admin può aggiornare il proprio username e password:
@app.put("/admin/update")
def update_admin(
current_password: str = Body(...),
new_username: str = Body(None),
new_password: str = Body(None),
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Accesso negato: solo l'admin può aggiornare i propri dati.")
if not pwd_context.verify(current_password, current_user.password):
raise HTTPException(status_code=400, detail="Password attuale errata")
if new_username:
current_user.username = new_username
if new_password:
current_user.password = pwd_context.hash(new_password)
db.commit()
return {"info": "Admin data updated successfully"}
Controlla se l’utente è admin.
Se la password attuale è errata, restituisce errore 400 – Password attuale errata.
Permette all’admin di cambiare username e password.
Salva le modifiche nel database.
Eliminazione di un utente
L’admin può eliminare solo utenti normali, non può eliminare se stesso:
@app.delete("/users/delete/")
def delete_user(
request: DeleteUserRequest,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db),
):
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Only admin can delete users")
if current_user.username == request.username:
raise HTTPException(status_code=403, detail="Admin cannot delete themselves")
user = db.query(User).filter(User.username == request.username).first()
if not user:
raise HTTPException(status_code=404, detail="User not found")
db.delete(user)
db.commit()
return {"message": f"User '{request.username}' deleted successfully"}
Solo un admin può eliminare utenti.
L’admin non può eliminare se stesso.
Se l’utente non esiste, restituisce errore 404 – User not found.
Se l’utente esiste, viene eliminato dal database.
Elenco utenti
L’admin può ottenere la lista di tutti gli utenti registrati:
@app.get("/users")
def list_users(current_user: User = Depends(get_current_user), db: Session = Depends(get_db)):
if not current_user.is_admin:
raise HTTPException(status_code=403, detail="Accesso negato: solo l'admin può visualizzare gli utenti.")
users = db.query(User).all()
return [
{
"id": user.id,
"username": user.username,
"is_admin": user.is_admin
}
for user in users
]
Solo l’admin può accedere all’elenco degli utenti.
Se non è admin, restituisce 403 – Accesso negato.
Restituisce la lista di utenti con:
- ID
- Username
- Ruolo (Admin o normale).
Ottenere la configurazione del sistema
Questo endpoint restituisce i parametri di configurazione salvati nei file config.ini e motion.conf:
@app.get("/config")
def get_config():
config_data = {}
# General configuration and notifications from config.ini
config.read(config_file_path) # Always reload the file
config_data["general"] = dict(config.items("general"))
config_data["notification"] = dict(config.items("notification"))
# motion.conf configuration
try:
with open(motion_config_path, "r") as f:
motion_config_content = f.readlines()
config_data["motion"] = {"motion_conf_content": "".join(motion_config_content)}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error reading motion.conf: {e}")
print("Motion.conf returned to the frontend:")
print(config_data["motion"]["motion_conf_content"])
return config_data
Legge il file config.ini e ne estrae le sezioni general e notification.
Apre il file motion.conf per leggere i parametri di configurazione del motion detection.
Se il file motion.conf non può essere letto, restituisce un errore 500.
Restituisce le configurazioni al frontend in formato JSON.
Aggiornamento della configurazione
Permette di modificare i parametri nei file config.ini e motion.conf:
@app.post("/config")
def update_config(new_config: dict = Body(...)):
try:
print("Data received for update:", new_config)
# General configuration update and notifications in config.ini
for section, params in new_config.items():
if section in ["general", "notification"]:
if section not in config.sections():
config.add_section(section)
for key, value in params.items():
config.set(section, key, str(value))
with open(config_file_path, 'w') as configfile:
config.write(configfile)
# Reload to confirm saving
config.read(config_file_path)
print("Config.ini updated successfully.")
# Updated motion.conf
if "motion" in new_config and "motion_conf_content" in new_config["motion"]:
motion_config_content = new_config["motion"]["motion_conf_content"]
with open(motion_config_path, 'w') as motion_file:
motion_file.write(motion_config_content)
print("Motion.conf updated successfully.")
return {"info": "Configuration updated successfully."}
except Exception as e:
print("Error during update:", e)
raise HTTPException(status_code=500, detail=f"Error during update: {e}")
Riceve i nuovi parametri in un oggetto JSON.
Aggiorna il file config.ini, modificando le sezioni general e notification.
Sovrascrive il file motion.conf con i nuovi parametri ricevuti.
Se tutto va bene, restituisce “Configuration updated successfully”.
Se c’è un errore, restituisce 500 – Error during update.
Invio notifiche
Questo endpoint gestisce l’invio di notifiche tramite diversi metodi (Pushover, Email, Telegram):
@app.post("/notify")
def notify(title: str = Body(...), message: str = Body(...), image_path: str = Body(None)):
# Load the updated configuration file
config.read(config_file_path)
# Check and send notification for each enabled method
try:
send_pushover_notification(title, message, image_path)
except Exception as e:
print(f"[Notification] Error during pushover notification {e}")
try:
send_mail_notification(title, message, image_path)
except Exception as e:
print(f"[Notification] Error during Gmail notification {e}")
try:
send_telegram_notification(title, message, image_path)
except Exception as e:
print(f"[Notification] Error during Telegram notification {e}")
return {"info": "Notifications sent based on active settings"}
Legge la configurazione aggiornata per capire quali notifiche inviare.
Invia notifiche tramite:
- Pushover
- Email (Gmail)
- Telegram
Se una notifica fallisce, stampa un errore nel log ma continua con gli altri metodi.
Restituisce “Notifications sent based on active settings”.
Upload di una foto
Endpoint per caricare una singola immagine nel sistema:
@app.post("/upload/")
async def upload_photo(file: UploadFile = File(...), db: Session = Depends(get_db)):
# Save the original photo
file_location = f"photos/{file.filename}"
with open(file_location, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# Create and save the thumbnail
image = Image.open(file_location)
image.thumbnail((128, 128)) # Creation of the thumbnail with dimensions of 128x128
thumbnail_location = f"photos/thumbnails/{file.filename}"
image.save(thumbnail_location)
# Save the information in the database for the photo and thumbnail
new_photo = Photo(filename=file.filename, filepath=file_location, thumbnail_path=thumbnail_location)
db.add(new_photo)
db.commit()
db.close()
# Send notification
send_pushover_notification(
title="New photo uploaded",
message=f"The photo {file.filename} has been uploaded successfully.",
image_path=thumbnail_location
)
return {"info": f"File '{file.filename}' and thumbnail uploaded successfully!"}
Salva la foto nella cartella photos/.
Genera una miniatura 128×128 e la salva in photos/thumbnails/.
Registra il file nel database con il suo percorso.
Invia una notifica Pushover informando l’utente del caricamento.
Restituisce “File uploaded successfully”.
Upload multiplo di foto
Questo endpoint permette di caricare più immagini in un’unica richiesta:
@app.post("/upload_batch/")
async def upload_photos_batch(files: List[UploadFile] = File(...), db: Session = Depends(get_db)):
for file in files:
# Saving the photo
file_location = f"photos/{file.filename}"
with open(file_location, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
# Creating the thumbnail
thumbnail_location = f"photos/thumbnails/{file.filename}"
image = Image.open(file_location)
image.thumbnail((128, 128))
image.save(thumbnail_location)
# Insertion into the database
new_photo = Photo(filename=file.filename, filepath=file_location, thumbnail_path=thumbnail_location)
db.add(new_photo)
db.commit() # Commit to save all photos in one go
return {"info": f"Batch of {len(files)} photos uploaded successfully!"}
Itera sui file ricevuti e li salva in photos/.
Genera miniature per ogni immagine e le salva in photos/thumbnails/.
Registra ogni immagine nel database.
Effettua un unico commit, riducendo il numero di scritture sul database.
Restituisce “Batch of X photos uploaded successfully!”.
Recupero della lista di foto con paginazione
Questo endpoint permette di ottenere una lista paginata delle immagini archiviate:
@app.get("/photos/")
def list_photos(page: int = 1, page_size: int = 10, db: Session = Depends(get_db)):
all_photos = db.query(Photo).order_by(Photo.id.desc()).all() # We retrieve all photos sorted by descendant ID
total_photos = len(all_photos) # Let's count the total number of photos
start = (page - 1) * page_size # Let's calculate the beginning
end = start + page_size # Let's calculate the end
# If start is out of range of photos, we return an empty list
if start >= total_photos:
return []
# Let's take the sublist between start and end
paginated_photos = all_photos[start:end]
paginated_photos_data = [{"id": photo.id, "filename": photo.filename, "thumbnail": photo.thumbnail_path} for photo in paginated_photos]
db.close()
return paginated_photos_data
Ordina le foto in ordine decrescente di ID, in modo che le più recenti vengano mostrate per prime.
Applica la paginazione:
- page: numero della pagina richiesta.
- page_size: numero di immagini per pagina.
Calcola gli indici per selezionare solo le immagini necessarie.
Se la pagina richiesta è oltre il numero totale di immagini, restituisce una lista vuota.
Chiude la connessione al database per ottimizzare l’uso delle risorse.
Calcolo del numero totale di foto
Endpoint che restituisce il numero totale di immagini e pagine disponibili:
@app.get("/photos/count/")
def get_photo_count(page_size: int = 10, db: Session = Depends(get_db)):
total_photos = db.query(Photo).count() # Count all photos in database
total_pages = (total_photos + page_size - 1) // page_size # Calculate the number of pages
return {
"total_photos": total_photos,
"total_pages": total_pages,
"page_size": page_size
}
Conta il numero totale di foto archiviate nel database.
Calcola il numero di pagine in base alla grandezza della pagina richiesta.
Restituisce i dati in formato JSON, inclusi:
- total_photos: numero totale di immagini archiviate.
- total_pages: numero totale di pagine disponibili.
- page_size: numero di immagini per pagina.
Ricerca di foto per nome
Consente di cercare immagini specifiche filtrando per nome:
@app.get("/photos/search")
def search_photos(filename: str = None, db: Session = Depends(get_db)):
query = db.query(Photo)
if filename:
query = query.filter(Photo.filename.contains(filename))
photos = query.all()
db.close()
return [{"id": photo.id, "filename": photo.filename} for photo in photos]
Cerca immagini in base al nome del file.
Se viene passato un parametro filename, effettua un filtro nel database.
Restituisce i risultati con ID e nome file.
Chiude la connessione al database per risparmiare risorse.
Recupero dei metadati di una foto
Consente di ottenere informazioni dettagliate su una specifica immagine:
@app.get("/photos/{photo_id}")
def get_photo_metadata(photo_id: int, db: Session = Depends(get_db)):
photo = db.query(Photo).filter(Photo.id == photo_id).first()
if not photo:
raise HTTPException(status_code=404, detail="Photo not found")
return {"id": photo.id, "filename": photo.filename, "filepath": photo.filepath}
Cerca l’immagine nel database usando l’ID.
Se non esiste, restituisce 404 – Photo not found.
Se trovata, restituisce:
- ID
- Nome file
- Percorso del file.
Download di una foto
Permette di scaricare una foto con il nome originale:
@app.get("/download/{photo_id}")
def download_photo(photo_id: int, db: Session = Depends(get_db)):
photo = db.query(Photo).filter(Photo.id == photo_id).first()
if not photo:
raise HTTPException(status_code=404, detail="Photo not found")
return FileResponse(photo.filepath, media_type="image/jpeg", filename=photo.filename)
Verifica se la foto esiste nel database.
Se non esiste, restituisce 404 – Photo not found.
Se esiste, restituisce il file con il suo nome originale.
Download della miniatura di una foto
Consente di scaricare la versione ridotta di un’immagine:
@app.get("/download-thumbnail/{photo_id}")
def download_thumbnail(photo_id: int, db: Session = Depends(get_db)):
photo = db.query(Photo).filter(Photo.id == photo_id).first()
if not photo:
raise HTTPException(status_code=404, detail="Photo not found")
thumbnail_path = os.path.join("photos/thumbnails", photo.filename)
if os.path.exists(thumbnail_path):
return FileResponse(thumbnail_path, media_type="image/jpeg", filename=f"thumb-{photo.filename}")
else:
raise HTTPException(status_code=404, detail="Thumbnail not found")
Cerca la foto nel database usando l’ID.
Se non esiste, restituisce 404 – Photo not found.Verifica se il file della miniatura esiste.
Se non esiste, restituisce 404 – Thumbnail not found.
Se la miniatura esiste, la restituisce come file scaricabile.
Eliminazione di una foto
Permette di eliminare una foto e la sua miniatura dal sistema:
@app.delete("/photos/{photo_id}")
def delete_photo(photo_id: int, db: Session = Depends(get_db)):
# Find the photo you want to delete
photo = db.query(Photo).filter(Photo.id == photo_id).first()
if not photo:
db.close()
return {"error": "Photo not found"}
# We delete the original image file if it exists
if os.path.exists(photo.filepath):
os.remove(photo.filepath)
# We delete the thumbnail if it exists
if os.path.exists(photo.thumbnail_path):
os.remove(photo.thumbnail_path)
# We delete the entry from the database
db.delete(photo)
db.commit()
db.close()
return {"info": f"Photo '{photo.filename}' and thumbnail successfully deleted!"}
Verifica se la foto esiste nel database.
Se non esiste, restituisce “Photo not found”.
Elimina il file originale dal filesystem.
Elimina la miniatura corrispondente.
Elimina il record dal database.
Restituisce “Photo deleted successfully”.
Upload di un video
Questo endpoint permette di caricare un video nel sistema, generare una miniatura e salvarlo nel database:
@app.post("/upload_video/")
async def upload_video(file: UploadFile = File(...), db: Session = Depends(get_db)):
video_location = f"videos/{file.filename}"
motion_video_location = f"videos/motion/{file.filename}"
thumbnail_location = f"videos/thumbnails/{file.filename}.jpg"
try:
# 1. Save the original video
with open(video_location, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
print(f"[UPLOAD] Video saved: {video_location}")
# Set read/write permissions for everyone (optional)
os.chmod(video_location, 0o666) # Permissions: rw-rw-rw-
# 2. Check the video
try:
with VideoFileClip(video_location) as clip:
duration = clip.duration
if duration <= 0:
print(f"[VALIDATION] Invalid duration for video: {video_location}")
raise HTTPException(status_code=400, detail="Invalid video or invalid duration")
print(f"[VALIDATION] Valid video with duration: {duration} seconds")
except Exception as e:
print(f"[VALIDATION] Error during video validation: {e}")
raise HTTPException(status_code=400, detail="Invalid or corrupt video")
# 3. Thumbnail creation
try:
with VideoFileClip(video_location) as clip:
frame = clip.get_frame(1) # It takes the frame at the first second
from PIL import Image
image = Image.fromarray(frame)
image.save(thumbnail_location)
print(f"[THUMBNAIL] Successfully created: {thumbnail_location}")
except Exception as e:
print(f"[THUMBNAIL] Error creating thumbnail: {e}")
raise HTTPException(status_code=500, detail="Error creating thumbnail")
# 4. Save to database
try:
existing_video = db.query(Video).filter(Video.filename == file.filename).first()
if existing_video:
print(f"[DATABASE] Video already present: {file.filename}")
raise HTTPException(status_code=400, detail="Video already present in the database")
new_video = Video(
filename=file.filename,
filepath=video_location,
thumbnail_path=thumbnail_location,
duration=duration
)
db.add(new_video)
db.commit()
print(f"[DATABASE] Video saved in database: {file.filename}")
except Exception as e:
print(f"[DATABASE] Error saving to database: {e}")
raise HTTPException(status_code=500, detail="Error saving to database")
# 5. Send notification
try:
notify_response = requests.post(
"http://localhost:8000/notify",
json={
"title": "New video uploaded",
"message": f"The video {file.filename} has been uploaded successfully.",
"image_path": thumbnail_location
},
timeout=30
)
if notify_response.status_code != 200:
print(f"[NOTIFICATION] Error: {notify_response.status_code} - {notify_response.text}")
#raise HTTPException(status_code=500, detail="Error sending notification")
print("[NOTIFICATION] Sent successfully.")
except requests.Timeout:
print("[NOTIFICATION] Timeout while sending.")
#raise HTTPException(status_code=500, detail="Timeout while sending notification")
except Exception as e:
print(f"[NOTIFICATION] Error: {e}")
#raise HTTPException(status_code=500, detail=f"Error sending notification: {e}")
except Exception as e:
print(f"[ERROR] {e}")
# Delete the video from the videos/motion folder
print(f"[CLEANUP] {motion_video_location}")
if os.path.exists(motion_video_location):
print(f"[DEBUG] Found video to delete: {motion_video_location}")
os.remove(motion_video_location)
print(f"[DEBUG] Video successfully deleted: {motion_video_location}")
else:
print(f"[DEBUG] File not found: {motion_video_location}")
Salva il file video in videos/.
Controlla la validità del video (durata maggiore di 0 secondi).
Crea una miniatura catturando un frame dal primo secondo.
Salva il video nel database, evitando duplicati.
Invia una notifica all’utente.
Elimina il file temporaneo dalla cartella videos/motion/.
Recupero della lista di video con paginazione
Consente di ottenere un elenco di video archiviati, con supporto alla paginazione:
@app.get("/videos/")
def list_videos(page: int = 1, page_size: int = 10, db: Session = Depends(get_db)):
# Get all videos from database, sorted by descendant ID
all_videos = db.query(Video).order_by(Video.id.desc()).all()
# Total videos
total_videos = len(all_videos)
# Calculation of indexes for pagination
start = (page - 1) * page_size
end = start + page_size
# Check for out-of-range indices
if start >= total_videos:
print("Indices out of range: no video available for this page.")
return []
# Retrieving videos for the current page
paginated_videos = all_videos[start:end]
paginated_videos_data = [{"id": video.id, "filename": video.filename, "thumbnail": video.thumbnail_path} for video in paginated_videos]
return paginated_videos_data
Ordina i video in ordine decrescente di ID.
Applica la paginazione, mostrando solo un certo numero di video per pagina.
Restituisce una lista JSON con:
- ID
- Nome del file
- Percorso della miniatura.
Recupero del numero totale di video
Restituisce il numero totale di video salvati nel sistema:
@app.get("/videos/count/")
def get_video_count(page_size: int = 10, db: Session = Depends(get_db)):
total_videos = db.query(Video).count()
total_pages = (total_videos + page_size - 1) // page_size
return {
"total_videos": total_videos,
"total_pages": total_pages,
"page_size": page_size
}
Conta il numero totale di video nel database.
Calcola il numero di pagine basandosi sulla grandezza della pagina richiesta.
Restituisce le informazioni in JSON.
Download della miniatura di un video
Permette di scaricare la miniatura di un video:
@app.get("/videos/thumbnails/{filename}")
def get_video_thumbnail(filename: str):
thumbnail_path = os.path.join(VIDEO_THUMBNAIL_DIRECTORY, filename)
if os.path.exists(thumbnail_path):
return FileResponse(thumbnail_path, media_type="image/jpeg")
else:
raise HTTPException(status_code=404, detail="Thumbnail not found")
Verifica se la miniatura esiste.
Se il file è presente, lo restituisce come immagine.
Se il file non esiste, restituisce 404 – Thumbnail not found.
Download di un video
Permette di scaricare un video dal sistema:
@app.get("/videos/download/{video_id}")
def serve_video_by_id(video_id: int, db: Session = Depends(get_db)):
video = db.query(Video).filter(Video.id == video_id).first()
if not video:
raise HTTPException(status_code=404, detail="Video not found")
video_path = video.filepath
print(f"[DEBUG] Video required for ID {video_id}: {video_path}")
if os.path.exists(video_path):
return FileResponse(video_path, media_type="video/mp4")
else:
raise HTTPException(status_code=404, detail="Video not found")
Recupera il video dal database usando l’ID.
Se il file esiste, lo restituisce come file MP4.
Se il file non esiste, restituisce 404 – Video not found.
Riavvio del servizio tramite API Flask
Questo endpoint invia una richiesta HTTP a un’API Flask per riavviare il servizio:
@app.post("/restart-service")
def restart_service_via_flask():
flask_api_url = "http://192.168.1.190:5000/restart-service" # Flask API URL
try:
response = requests.post(flask_api_url, timeout=10) # 10 second safety timeout
if response.status_code == 200:
return {"info": "Service restart completed via Flask API."}
else:
return {"error": f"Error while restarting: {response.status_code} - {response.text}"}
except requests.RequestException as e:
return {"error": f"Error communicating with the Flask API: {str(e)}"}
Invia una richiesta POST all’API Flask sulla rete locale (192.168.1.190:5000).
Imposta un timeout di 10 secondi per evitare blocchi.
Se il servizio risponde con 200 OK, conferma che il riavvio è stato completato.
Se la richiesta fallisce, restituisce un messaggio di errore.
Invio di notifiche via email (SMTP)
Questa funzione invia notifiche email tramite un server SMTP (es. Gmail):
def send_mail_notification(title, message, image_path=None):
if config.get('notification', 'gmail_enabled').lower() != 'true':
print("Email notification disabled.")
return
# SMTP configurations
smtp_server = config.get('notification', 'smtp_server')
smtp_port = config.get('notification', 'smtp_port')
sender_email = config.get('notification', 'sender_email')
app_password = config.get('notification', 'app_password')
receiver_email = config.get('notification', 'receiver_email')
# Composing the email
msg = MIMEMultipart()
msg['From'] = sender_email
msg['To'] = receiver_email
msg['Subject'] = title
# Body of the message
msg.attach(MIMEText(message, 'plain'))
# Attach image, if present
if image_path:
with open(image_path, 'rb') as img:
img_data = img.read()
image = MIMEImage(img_data, name="notification_image.jpg")
msg.attach(image)
# Sending email via Gmail SMTP
try:
with smtplib.SMTP(smtp_server, smtp_port) as server:
server.starttls()
server.login(sender_email, app_password)
server.sendmail(sender_email, receiver_email, msg.as_string())
print("Email sent successfully with Gmail.")
except Exception as e:
print(f"Error sending email with Gmail: {e}")
Legge la configurazione SMTP per ottenere i dettagli del server email.
Crea un’email con titolo e corpo del messaggio.
Allega un’immagine (se presente), utile per notifiche con anteprima.
Invia il messaggio tramite SMTP.
Se l’invio fallisce, stampa un errore nei log.
Invio di notifiche via Pushover
Pushover è un servizio di notifica per dispositivi mobili e desktop:
def send_pushover_notification(title, message, image_path=None):
# Checks if Pushover is enabled
if config.get('notification', 'pushover_enabled').lower() != 'true':
print("Pushover notification disabled.")
return
# Your Pushover API credentials
api_token = config.get('notification', 'pushover_token') # The application token
user_key = config.get('notification', 'pushover_user_key') # Your user key
# Basic notification parameters
data = {
"token": api_token,
"user": user_key,
"title": title,
"message": message,
}
# Check if there is an image to attach (e.g. the thumbnail)
if image_path:
with open(image_path, "rb") as image_file:
response = requests.post("https://api.pushover.net/1/messages.json", data=data, files={"attachment": image_file})
else:
response = requests.post("https://api.pushover.net/1/messages.json", data=data)
# Check the response from the API
if response.status_code == 200:
print("Pushover notification sent successfully")
else:
print(f"Error sending pushover notification: {response.status_code} - {response.text}")
Controlla se le notifiche Pushover sono abilitate.
Carica le credenziali API (token e user key).
Invia una richiesta HTTP a Pushover, con o senza immagine allegata.
Verifica la risposta API, stampando errori in caso di problemi.
Invio di notifiche via Telegram
Telegram permette di inviare messaggi e immagini ai bot registrati:
def send_telegram_notification(title, message, image_path=None):
# Check if Telegram is enabled
if config.get('notification', 'telegram_enabled').lower() != 'true':
print("Telegram notification disabled.")
return
# Retrieve the parameters from the configuration file
token = config.get('notification', 'telegram_token')
chat_id = config.get('notification', 'chatid_telegram')
telegram_api_url = f"https://api.telegram.org/bot{token}/sendMessage"
# Configure the message
data = {
"chat_id": chat_id,
"text": f"{title}\n\n{message}"
}
# Send the text message
response = requests.post(telegram_api_url, data=data)
if response.status_code == 200:
print("Telegram notification sent successfully")
else:
print(f"Error sending Telegram notification: {response.status_code} - {response.text}")
# Send image as separate file (if exists)
if image_path:
telegram_photo_url = f"https://api.telegram.org/bot{token}/sendPhoto"
with open(image_path, "rb") as photo:
response = requests.post(telegram_photo_url, data={"chat_id": chat_id}, files={"photo": photo})
if response.status_code == 200:
print("Telegram photo sent successfully")
else:
print(f"Error sending Telegram photo: {response.status_code} - {response.text}")
Verifica se le notifiche Telegram sono abilitate.
Carica il token e l’ID della chat dal file di configurazione.
Invia un messaggio testuale via API Telegram.
Se presente, invia anche un’immagine come file allegato.
Verifica la risposta dell’API, stampando errori in caso di problemi.
Lo script motion_monitor.py: il supervisore dei file multimediali
Lo script motion_monitor.py ha il compito di monitorare le cartelle in cui vengono salvati i file da Motion, il software che gestisce il rilevamento di movimento. Questo script è responsabile del monitoraggio dei file multimediali (foto e video) generati dal software di motion detection. Il suo compito principale è rilevare nuovi file, verificarne la stabilità e caricarli tramite API. Inoltre, gestisce la pulizia dei file più vecchi per mantenere sotto controllo l’archiviazione.
Funzionalità principali
- Monitoraggio continuo delle cartelle di foto e video: rileva nuovi file generati da Motion.
- Batch processing: per ottimizzare l’invio di file al backend, le immagini e i video vengono caricati in gruppi invece di essere inviati singolarmente.
- Validazione file: verifica che i video siano completi e privi di errori prima di caricarli nel database.
- Gestione dello spazio di archiviazione: elimina automaticamente i file più vecchi quando viene superato il numero massimo impostato in configurazione.
Nota: questo script opera in loop continuo, regolato da parametri configurabili nel file config.ini.
Importazione delle librerie
import configparser
import os
import time
import requests
from contextlib import ExitStack
import psutil
from moviepy.editor import VideoFileClip
import shutil
from database import get_db, Photo, Video
configparser: legge il file di configurazione config.ini.
os, time, shutil: gestione dei file, delle directory e operazioni temporali.
requests: per inviare richieste HTTP (upload e notifiche).
psutil: controllo dei processi per verificare se un file è in uso.
moviepy.editor.VideoFileClip: analisi dei video per verificare durata e stabilità.
ExitStack: gestione efficiente dell’apertura e chiusura di più file.
database (SQLAlchemy): interazione con il database per gestire foto e video.
Configurazione e parametri
MAX_STABILITY_CHECKS = 5 # Maximum number of file stability checks
STABILITY_INTERVAL = 2 # Interval in seconds between each check
MAX_STABILITY_CHECKS: numero massimo di tentativi per verificare se un file è stabile.
STABILITY_INTERVAL: intervallo di tempo tra un controllo e l’altro.
Caricamento del file di configurazione
config = configparser.ConfigParser()
config.read('config.ini')
Carica il file di configurazione config.ini per leggere i parametri del sistema.
INIT_DELAY = config.getint('general', 'init_delay')
PROCESS_INTERVAL = config.getint('general', 'process_interval')
MAX_PHOTOS = config.getint('general', 'max_photos')
MAX_VIDEOS = config.getint('general', 'max_videos')
BATCH_SIZE = config.getint('general', 'batch_size')
INIT_DELAY: ritardo iniziale prima di elaborare i file.
PROCESS_INTERVAL: tempo di attesa tra due scansioni della cartella.
MAX_PHOTOS / MAX_VIDEOS: numero massimo di foto e video conservati.
BATCH_SIZE: numero massimo di file da caricare in un batch.
Percorsi delle cartelle
PHOTOS_DIRECTORY = config.get('paths', 'photo_directory')
VIDEOS_DIRECTORY = config.get('paths', 'video_directory')
PHOTOS_DIR = config.get('paths','photos_dir')
VIDEOS_DIR = config.get('paths', 'videos_dir')
THUMBNAIL_DIRECTORY = config.get('paths','thumbnail_directory')
VIDEO_THUMBNAIL_DIRECTORY = config.get('paths','video_thumbnail_directory')
Percorsi delle directory dove Motion salva le immagini e i video.
URL delle API
BATCH_UPLOAD_API_URL = config.get('paths','batch_upload_api_url')
VIDEO_UPLOAD_API_URL = config.get('paths', 'upload_video_api_url')
NOTIFY_API_URL = config.get('paths','notify_api_url')
URL delle API per l’upload di foto/video e per inviare notifiche.
Inizializzazione delle variabili
start_time = time.time()
last_processed_time = time.time()
photo_batch = []
start_time: salva il timestamp di avvio dello script.
last_processed_time: tiene traccia dell’ultimo caricamento di file.
photo_batch: elenco delle foto in attesa di caricamento.
Creazione delle cartelle se non esistono
if not os.path.exists(PHOTOS_DIR):
os.makedirs(PHOTOS_DIR)
if not os.path.exists(VIDEOS_DIR):
os.makedirs(VIDEOS_DIR)
Controlla se le cartelle per le foto e i video esistono. Se non esistono, le crea.
Gestione dei file da 0 byte
zero_byte_count = {} # Global dictionary to track files to 0B
MAX_ZERO_BYTE_CHECKS = 50 # Maximum threshold for files at 0B
Controlla se un video rimane a 0 byte per troppo tempo. Se rimane a 0 byte per poco tempo significa che motion lo sta salvando gradualmente. Se rimane a 0 byte per molto tempo significa che è corrotto e verrà eliminato.
Determinazione della modalità di Motion
def get_mode_from_motion_conf(motion_config_path):
try:
with open(motion_config_path, 'r') as f:
lines = f.readlines()
mode = "unknown"
for line in lines:
line = line.strip()
if line.startswith("output_pictures") and "on" in line:
mode = "photos"
elif line.startswith("ffmpeg_output_movies") and "on" in line:
mode = "videos"
return mode
except FileNotFoundError:
print("The motion.conf file was not found.")
return "unknown"
Legge il file di configurazione motion.conf e determina se Motion è in modalità foto o video.
Verifica se un file è in uso
def is_file_in_use(file_path):
"""Check if the file is in use."""
for proc in psutil.process_iter(['pid', 'name', 'open_files']):
try:
open_files = proc.info['open_files']
if open_files:
for open_file in open_files:
if open_file.path == file_path:
print(f"The file {file_path} is still in use by the process {proc.info['name']} (PID: {proc.info['pid']}).")
return True
except psutil.AccessDenied:
continue
except psutil.NoSuchProcess:
continue
return False
Verifica se un file è ancora in uso da parte di un altro processo.
Verifica la stabilità dei video
def is_video_ready(video_path):
"""
Check if the video file is stable and valid for uploading.
"""
try:
current_size = os.path.getsize(video_path)
if current_size == 0:
os.remove(video_path)
return False
previous_size = current_size
for i in range(MAX_STABILITY_CHECKS):
time.sleep(STABILITY_INTERVAL)
current_size = os.path.getsize(video_path)
if current_size == previous_size and not is_file_in_use(video_path):
break
previous_size = current_size
else:
return False
with VideoFileClip(video_path) as clip:
if clip.duration <= 0:
return False
return True
except Exception as e:
return False
Elimina i video che sono rimasti a 0 byte per troppo tempo.
Controlla se il file ha finito di crescere prima di considerarlo stabile.
Verifica la durata del video per assicurarsi che non sia corrotto.
Upload dei video in batch
def upload_videos_batch(video_batch):
print(f"[BATCH] Starting batch loading of {len(video_batch)} video...")
for video in video_batch:
video_path = os.path.join(VIDEOS_DIR, video)
video_path = os.path.normpath(video_path)
try:
if not os.path.exists(video_path):
print(f"[UPLOAD ERROR] Video not found for upload: {video_path}")
continue
with open(video_path, "rb") as video_file:
response = requests.post(
VIDEO_UPLOAD_API_URL,
files={"file": video_file}
)
response.raise_for_status()
print(f"[UPLOAD SUCCESS] Video uploaded: {video_path}")
except Exception as e:
print(f"[UPLOAD ERROR] Error loading video {video_path}: {e}")
Carica tutti i video presenti nella lista video_batch.
Verifica se il file esiste realmente prima di procedere. Se l’upload ha successo, stampa [UPLOAD SUCCESS], altrimenti mostra un errore.
Upload delle foto in batch
def upload_photos_batch():
global photo_batch
files = []
with ExitStack() as stack:
for photo in photo_batch:
photo_path = os.path.join(PHOTOS_DIR, photo)
try:
files.append(("files", (photo, stack.enter_context(open(photo_path, "rb")), "image/jpeg")))
except FileNotFoundError:
print(f"Photo not found for batch upload: {photo_path}")
if files:
response = requests.post(BATCH_UPLOAD_API_URL, files=files)
if response.status_code == 200:
print(f"Batch of {len(photo_batch)} photos uploaded successfully")
first_photo = photo_batch[0]
notify_response = requests.post(NOTIFY_API_URL, json={
"title": "New batch of photos uploaded",
"message": f"{len(photo_batch)} new photos uploaded successfully.",
"image_path": f"photos/thumbnails/{first_photo}"
})
for photo in photo_batch:
photo_path = os.path.join(PHOTOS_DIR, photo)
if os.path.exists(photo_path):
os.remove(photo_path)
print(f"Temporary photo removed: {photo_path}")
else:
print(f"Batch upload error: {response.status_code} - {response.text}")
photo_batch.clear()
Prepara le foto per l’upload in batch.
Invia una richiesta HTTP all’API BATCH_UPLOAD_API_URL.
Se l’upload ha successo, invia una notifica con il thumbnail della prima foto.
Elimina le foto caricate dalla cartella.
Monitoraggio dei file
Il cuore dello script è la funzione monitor_files(), che controlla continuamente la cartella di Motion, processa nuovi file, e pulisce quelli vecchi. Essendo una funzione molto lunga e complessa la esamineremo a pezzi.
def monitor_files():
global last_processed_time
while True:
mode = get_mode_from_motion_conf('/etc/motion/motion.conf')
Determina se Motion sta scattando foto o registrando video
Gestione delle foto
if mode == "photos":
print("Photo mode active. Scanning the photo folder...")
photos = sorted(os.listdir(PHOTOS_DIR))
for photo in photos:
photo_path = os.path.join(PHOTOS_DIR, photo)
current_time = time.time()
if current_time - start_time < INIT_DELAY:
os.remove(photo_path)
print(f"Photo skipped during startup: {photo_path}")
continue
photo_batch.append(photo)
print(f"Adding photo to batch: {photo}")
if len(photo_batch) >= BATCH_SIZE:
print("Batch threshold reached, loading...")
upload_photos_batch()
last_processed_time = time.time()
Controlla la cartella delle foto e ordina i file. Se lo script è appena stato avviato capita che Motion inizi a scattare raffiche di foto perchè si sta adattando alle condizioni abientali che per lui non sono ancora stabili, quindi lo script provvede ad eliminare le foto scattate in un certo intervallo di tempo dal suo avvio (evita errori di startup).
Aggiunge le foto alla coda di upload. Se la coda raggiunge BATCH_SIZE, carica le foto.
Pulizia automatica delle foto
photos_to_be_deleted = sorted(
[photo for photo in os.listdir(PHOTOS_DIRECTORY) if photo not in ("motion", "thumbnails")],
key=lambda f: os.path.getmtime(os.path.join(PHOTOS_DIRECTORY, f))
)
if len(photos_to_be_deleted) > MAX_PHOTOS and not photo_batch:
print(f"[CLEANUP] Photo number: {len(photos_to_be_deleted)}")
print(f"[CLEANUP] Maximum number of photos: {MAX_PHOTOS}")
db = next(get_db())
try:
for old_photo in photos_to_be_deleted[:len(photos_to_be_deleted) - MAX_PHOTOS]:
photo_path = os.path.join(PHOTOS_DIRECTORY, old_photo)
if os.path.exists(photo_path):
os.remove(photo_path)
print(f"[CLEANUP] Old photo removed: {photo_path}")
thumbnail_path = os.path.join(THUMBNAIL_DIRECTORY, old_photo)
if os.path.exists(thumbnail_path):
os.remove(thumbnail_path)
print(f"[CLEANUP] Photo thumbnail removed: {thumbnail_path}")
photo_entry = db.query(Photo).filter(Photo.filename == old_photo).first()
if photo_entry:
db.delete(photo_entry)
db.commit()
print(f"[CLEANUP] Removed database entry for photo: {old_photo}")
finally:
db.close()
Verifica se ci sono più foto di MAX_PHOTOS.
Ordina le foto dalla più vecchia alla più recente.
Elimina le più vecchie sia dal filesystem che dal database.
Gestione dei video
elif mode == "videos":
print("Video mode active. Scanning video folder...")
videos = sorted(os.listdir(VIDEOS_DIR))
processed_files = set()
video_batch = []
for video in videos:
video_path = os.path.join(VIDEOS_DIR, video)
current_time = time.time()
if video in processed_files:
continue
if not is_video_ready(video_path):
print(f"[SKIP] Video not ready: {video_path}")
continue
video_batch.append(video)
processed_files.add(video)
print(f"Added video to batch: {video}")
if len(video_batch) >= BATCH_SIZE:
print("Batch threshold reached, loading...")
upload_videos_batch(video_batch)
video_batch = []
Ordina i video per data di creazione.
Verifica che il video sia stabile con is_video_ready().
Carica i video in batch quando raggiunge BATCH_SIZE.
Pulizia automatica dei video
all_videos = sorted(
[f for f in os.listdir(VIDEOS_DIRECTORY) if f not in {"thumbnails", "motion"}],
key=lambda f: os.path.getmtime(os.path.join(VIDEOS_DIRECTORY, f))
)
if len(all_videos) > MAX_VIDEOS:
print(f"[CLEANUP] Video number: {len(all_videos)}")
print(f"[CLEANUP] Maximum number of videos: {MAX_VIDEOS}")
db = next(get_db())
try:
old_videos = all_videos[:len(all_videos) - MAX_VIDEOS]
for old_video in old_videos:
old_video_path = os.path.join(VIDEOS_DIRECTORY, old_video)
if os.path.exists(old_video_path):
os.remove(old_video_path)
print(f"[CLEANUP] Old video removed: {old_video_path}")
old_thumbnail_path = os.path.join(VIDEO_THUMBNAIL_DIRECTORY, f"{old_video}.jpg")
if os.path.exists(old_thumbnail_path):
os.remove(old_thumbnail_path)
print(f"[CLEANUP] Video thumbnail removed: {old_thumbnail_path}")
video_entry = db.query(Video).filter(Video.filename == old_video).first()
if video_entry:
db.delete(video_entry)
db.commit()
print(f"[CLEANUP] Removed database entry for video: {old_video}")
finally:
db.close()
Elimina i video più vecchi quando MAX_VIDEOS è superato.
Pulisce anche i record dal database.
Lo script database.py: la gestione del database
Questo script fornisce le funzionalità di connessione e gestione del database SQLite, che memorizza i metadati delle foto, dei video e degli utenti. Lo script database.py utilizza un ORM (Object-Relational Mapping) per gestire l’interazione con il database SQLite. Un ORM è una tecnica di programmazione che permette di manipolare un database relazionale utilizzando oggetti e metodi di un linguaggio di programmazione, invece di scrivere direttamente query SQL. Questo approccio semplifica lo sviluppo, rendendo il codice più leggibile e mantenibile, oltre a fornire un livello di astrazione che facilita il passaggio a diversi tipi di database senza modificare il codice applicativo.
Funzionalità principali
- Definizione del modello dati: utilizza SQLAlchemy per gestire la struttura del database.
- Interfaccia per il backend: fornisce funzioni per creare, leggere, aggiornare ed eliminare record nel database.
- Connessione automatica: gestisce la connessione al database evitando problemi di concorrenza tra le richieste API.
Nota: questo script viene utilizzato da main.py e motion_monitor.py per tutte le operazioni sui dati.
Il file database.py definisce il database dell’applicazione di videosorveglianza utilizzando SQLAlchemy, che è un ORM (Object-Relational Mapper) per interagire con il database SQLite senza scrivere direttamente query SQL e svolge le funzionalità seguenti:
Connessione al database
DATABASE_URL = "sqlite:///./data/videosurveillance_db.sqlite"
engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()
Il database è un file SQLite situato in ./data/videosurveillance_db.sqlite.
engine è l’oggetto che permette la connessione al database.
SessionLocal è un gestore di sessioni che permette di eseguire operazioni sul database senza chiudere e riaprire la connessione ogni volta.
Base è la classe base da cui derivano tutti i modelli delle tabelle.
Definizione delle tabelle
Vengono definite tre tabelle: Photo, Video e User, ognuna con i relativi campi.
Tabella Photo
class Photo(Base):
__tablename__ = "photos"
id = Column(Integer, primary_key=True, index=True)
filename = Column(String, unique=True, index=True)
filepath = Column(String)
thumbnail_path = Column(String)
Contiene le informazioni sulle foto salvate dal sistema. Ogni foto ha:
- id: identificativo numerico univoco.
- filename: nome del file.
- filepath: percorso del file originale.
- thumbnail_path: percorso della miniatura.
Tabella Video
class Video(Base):
__tablename__ = "videos"
id = Column(Integer, primary_key=True, index=True)
filename = Column(String, unique=True, index=True)
filepath = Column(String)
thumbnail_path = Column(String) # Thumbnail generated from video
duration = Column(String) # Video length, optional
Contiene le informazioni sui video registrati. Ogni video ha:
- id: identificativo numerico.
- filename: nome del file.
- filepath: percorso del file.
- thumbnail_path: percorso della miniatura generata dal video.
- duration: durata del video (stringa, può essere in formato hh:mm:ss).
Tabella User
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True, index=True)
password = Column(String) # Hashed password
is_admin = Column(Boolean, default=False) # Flag to distinguish admin and normal users
Contiene gli utenti registrati nel sistema. Ogni utente ha:
- id: identificativo numerico.
- username: nome utente univoco.
- password: password salvata in formato hashato (per sicurezza).
- is_admin: flag booleano per indicare se l’utente è amministratore.
Creazione delle tabelle nel database
Base.metadata.create_all(bind=engine)
Controlla se le tabelle esistono nel database e, in caso contrario, le crea automaticamente.
Funzione per ottenere una sessione di database
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
Questa funzione genera una sessione di database che può essere utilizzata nel codice per eseguire query. Quando la sessione non è più necessaria, viene chiusa automaticamente.
Lo script host_service.py: il gestore del riavvio del servizio
Lo script host_service.py è un piccolo server Flask che fornisce un’API per riavviare il servizio videosurveillance.service quando viene richiesto dall’interfaccia web.
Funzionalità principali
- Avvio di un server Flask: lo script avvia un piccolo server web che espone un’API REST.
- Gestione del riavvio del servizio: quando viene premuto il pulsante di riavvio nella pagina di configurazione, viene inviata una richiesta HTTP a questo script.
- Esecuzione di comandi di sistema: al ricevimento della richiesta, lo script esegue il comando:
sudo systemctl restart videosurveillance.service
per riavviare i servizi di backend e frontend senza dover accedere manualmente al terminale.
Nota: questo script viene avviato come servizio di sistema (host_service.service) per essere sempre attivo in background.
Il file host_service.py è un microservizio Flask che permette di riavviare il servizio di videosorveglianza (videosurveillance.service) tramite una richiesta HTTP. Questo consente all’utente di riavviare il sistema direttamente dall’interfaccia web, senza dover accedere manualmente al terminale della Raspberry Pi. Lo script svolge le seguenti funzioni:
Importazione delle librerie
from flask import Flask, jsonify
import subprocess
Flask viene utilizzato per creare un’API HTTP minimale.
jsonify serve per restituire risposte in formato JSON.
subprocess consente di eseguire comandi di sistema direttamente da Python.
Creazione dell’app Flask
app = Flask(__name__)
Crea un’istanza dell’app Flask che gestirà le richieste HTTP.
Definizione dell’endpoint per il riavvio del servizio
@app.route('/restart-service', methods=['POST'])
def restart_service():
try:
subprocess.run(["sudo", "systemctl", "restart", "videosurveillance.service"], check=True)
return jsonify({"status": "success", "message": "Service restarted successfully"}), 200
except subprocess.CalledProcessError as e:
return jsonify({"status": "error", "message": f"Failed to restart service: {e}"}), 500
Viene creato un endpoint POST raggiungibile all’URL /restart-service.
Quando viene chiamato, esegue il comando:
sudo systemctl restart videosurveillance.service
per riavviare il servizio di videosorveglianza.
Se il riavvio ha successo, restituisce una risposta JSON con codice 200:
{"status": "success", "message": "Service restarted successfully"}
Se il comando fallisce, restituisce una risposta JSON con codice 500 con il messaggio di errore.
Avvio del server Flask
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000) # Can use any available port
Il server Flask viene avviato in ascolto su tutte le interfacce di rete (0.0.0.0) e sulla porta 5000.
Questo consente al frontend di inviare una richiesta HTTP al microservizio per riavviare videosurveillance.service.
Questo script è un esempio di come un microservizio Flask possa essere usato per interagire con il sistema operativo e migliorare la gestione dell’applicazione.
L’interfaccia web: il punto d’incontro tra utente e sistema
Per interagire con il sistema di videosorveglianza, ho sviluppato un’interfaccia web moderna e intuitiva basata su React e che utilizza Vite come tool di build, comunica con il backend FastAPI tramite API REST. Questa interfaccia consente di accedere facilmente a tutte le funzionalità del sistema, come la visualizzazione delle immagini e dei video, la gestione delle configurazioni e il monitoraggio dello stato del sistema in tempo reale. Dal punto di vista tecnico, l’interfaccia è composta da diverse pagine e componenti che lavorano insieme per offrire un’esperienza fluida e user-friendly. Ogni componente è stato progettato per uno scopo preciso. In questa sezione esploreremo il codice che governa le principali pagine e componenti dell’interfaccia, evidenziando come queste comunicano con il backend per fornire dati aggiornati e gestire le operazioni richieste dall’utente.
I files sono principalmente distribuiti tra le cartelle frontend/src e frontend/src/components/.
File principali in frontend/src
- index.html
- Questo file è il punto di ingresso dell’applicazione React. Contiene il contenitore root (<div id=”root”></div>) dove React monta l’interfaccia utente.
- Include le configurazioni base per la pagina, come favicon, charset, e viewport.
- main.jsx
- Questo file è il punto di ingresso di React.
- Importa React e ReactDOM, monta il componente <App /> dentro l’elemento con id root in index.html.
- Usa StrictMode, che aiuta a individuare potenziali problemi nel codice.
- App.jsx
- È il componente principale dell’app, che gestisce il routing e la struttura dell’interfaccia.
- Usa React Router (react-router-dom) per definire le rotte delle pagine principali.
- Importa e integra diversi componenti come LoginPage, AdminDashboard, ThumbnailGrid, VideoGrid, ecc.
- config.js
- Contiene la configurazione globale dell’applicazione.
- Definisce l’API_BASE_URL, utilizzata da tutti i componenti per comunicare con il backend.
Componenti in frontend/src/components/
Questa cartella contiene i componenti principali utilizzati dall’applicazione.
- ThumbnailGrid.jsx
- Gestisce la visualizzazione delle immagini sotto forma di griglia.
- Comunica con il backend per ottenere le foto disponibili.
- Supporta la paginazione e il cambio della lingua.
- Usa un sistema di popup per visualizzare le immagini a schermo intero.
- VideoGrid.jsx
- Simile a ThumbnailGrid, ma per la gestione dei video.
- Mostra miniature dei video, che una volta cliccate aprono un popup con il video riproducibile.
- Supporta paginazione e cambio lingua.
- LoginPage.jsx
- Gestisce il login degli utenti.
- Invia le credenziali al backend e riceve un token di autenticazione.
- Salva il token in localStorage per sessioni future.
- AdminDashboard.jsx
- Interfaccia per gli amministratori.
- Mostra statistiche e controlli avanzati, come gestione utenti e configurazione.
- UserSettings.jsx
- Pagina per modificare la password dell’utente loggato.
- Invia la richiesta al backend per aggiornare la password.
- Mostra messaggi di errore o successo.
- StatisticsPage.jsx
- Mostra grafici e statistiche sull’utilizzo del sistema.
- Usa Chart.js per visualizzare i dati.
Il Config Manager
Il Config Manager è un’interfaccia utente web situata in videosurveillance/static/config_manager.html.
Il suo scopo principale è permettere all amministratore di modificare e gestire i parametri di configurazione del sistema, come i settaggi di sicurezza, le notifiche e i percorsi dei file.
Funzionalità principali
Caricamento e visualizzazione della configurazione: legge il file config.ini dal backend e mostra i valori attuali.
Modifica dei parametri: l’ amministratore può aggiornare impostazioni come max_photos, max_videos, i token delle notifiche, e altro.
Invio delle modifiche al backend: una volta aggiornati i parametri, il Config Manager invia una richiesta API per salvare i nuovi valori nel file di configurazione.
Validazione dei dati: verifica che i valori inseriti siano corretti prima di inviarli.

Struttura del file
Il file config_manager.html è composto da:
- HTML: struttura dell’interfaccia utente con campi di input e pulsanti.
- JavaScript: gestisce il caricamento, la modifica e l’invio delle configurazioni.
Vediamo ora in dettaglio la struttura dei vari files.
File: index.html – Punto di ingresso dell’applicazione React
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Videosurveillance App</title>
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
Il file index.html è il punto di ingresso principale dell’applicazione React.
Definisce la struttura di base della pagina HTML che ospita il frontend della videosorveglianza.
- Dichiarazione del tipo di documento (<!DOCTYPE html>) → specifica che il file è un documento HTML5.
- <html lang=”en”> → imposta la lingua principale della pagina su inglese, utile per SEO e accessibilità.
- <meta charset=”UTF-8″> → assicura la compatibilità con tutti i caratteri speciali internazionali.
- <meta name=”viewport” content=”width=device-width, initial-scale=1.0″> → rende il layout responsivo sui dispositivi mobili.
- <title> → definisce il titolo della scheda del browser.
- <link rel=”icon” href=”favicon.png”> → imposta l’icona della scheda del browser.
- <div id=”root”></div> → elemento principale dell’app React, dove verrà montata l’interfaccia.
- <script type=”module” src=”/src/main.jsx”></script> → importa e avvia React attraverso il file main.jsx.
File: main.jsx – Inizializzazione dell’app React
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));
Il file main.jsx si occupa di montare l’app React all’interno della pagina HTML e di avviare il rendering dell’interfaccia utente.
- Importa React → carica la libreria React, necessaria per la gestione dei componenti.
- Importa ReactDOM → carica ReactDOM, la libreria che consente di renderizzare l’app nel DOM.
- Importa il componente principale → importa App, che rappresenta il punto di ingresso dell’applicazione.
- Montaggio dell’applicazione → utilizza ReactDOM.render() per montare il componente App all’interno dell’elemento con id “root” definito in index.html.
File: App.jsx – Struttura principale dell’app React
import React, { useState, useEffect } from "react";
import { BrowserRouter as Router, Route, Routes, Link, Navigate } from "react-router-dom";
import ThumbnailGrid from "./components/ThumbnailGrid";
import VideoGrid from "./components/VideoGrid";
import LoginPage from "./components/LoginPage";
import AdminDashboard from "./components/AdminDashboard";
import UserSettings from "./components/UserSettings";
import StatisticsPage from "./components/StatisticsPage";
import config from './config';
import "./styles/App.css";
Il file App.jsx rappresenta il punto di ingresso dell’applicazione React e gestisce l’interfaccia principale, il routing e il sistema di autenticazione.
- Importa React e gli Hook → carica React, oltre agli hook useState e useEffect, utilizzati per la gestione dello stato e degli effetti collaterali.
- Importa React Router → include le funzioni di routing, come Router, Route, Routes, Link, Navigate, necessarie per la navigazione tra le pagine.
- Importa i componenti → carica i componenti principali dell’interfaccia: ThumbnailGrid, VideoGrid, LoginPage, AdminDashboard, UserSettings, StatisticsPage.
- Importa la configurazione → importa il file config.js, che contiene la configurazione dell’API.
- Importa i fogli di stile → carica App.css per lo stile dell’interfaccia.
Gestione dello stato e autenticazione
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAdmin, setIsAdmin] = useState(false);
isAuthenticated → tiene traccia dello stato di autenticazione dell’utente.
isAdmin → indica se l’utente autenticato è un amministratore.
Controllo del token di autenticazione
useEffect(() => {
const token = localStorage.getItem("token");
if (token) {
try {
const decodedToken = JSON.parse(atob(token.split(".")[1]));
const isTokenExpired = decodedToken.exp * 1000 < Date.now();
if (!isTokenExpired) {
setIsAuthenticated(true);
setIsAdmin(decodedToken.is_admin || false);
} else {
console.warn("Token expired. Removing token...");
localStorage.removeItem("token");
}
} catch (err) {
console.error("Invalid token:", err);
localStorage.removeItem("token");
}
}
}, []);
Legge il token salvato nel localStorage.
Decodifica il payload del token per verificare la data di scadenza.
Se il token è valido, imposta lo stato di autenticazione e verifica se l’utente è un admin.
Se il token è scaduto o invalido, lo elimina dal localStorage.
Gestione del login
const handleLogin = (token) => {
setIsAuthenticated(true);
localStorage.setItem("token", token);
try {
const decodedToken = JSON.parse(atob(token.split(".")[1]));
setIsAdmin(decodedToken.is_admin || false);
} catch (error) {
console.error("Token decoding error:", error);
setIsAdmin(false);
}
};
Quando l’utente effettua il login, salva il token e aggiorna lo stato.
Decodifica il token per verificare se l’utente è admin.
Gestione del logout
const handleLogout = () => {
setIsAuthenticated(false);
setIsAdmin(false);
localStorage.removeItem("token");
window.location.href = "/login";
};
Rimuove il token, reimposta lo stato e reindirizza alla pagina di login.
Navbar e navigazione
{isAuthenticated && (
<nav className="navbar">
<ul>
<li><Link to="/">Photos</Link></li>
<li><Link to="/videos">Videos</Link></li>
{!isAdmin && <li><Link to="/user-settings">User Settings</Link></li>}
{isAdmin && (
<>
<li><Link to="/admin-dashboard">Admin Dashboard</Link></li>
<li><Link to="/configuration">Configuration</Link></li>
</>
)}
<li><Link to="/statistics">Statistics</Link></li>
{isAdmin && (
<li>
<a href={`${config.API_BASE_URL}/download-backup`} download>Download Backup</a>
</li>
)}
<li>
<button onClick={handleLogout} className="logout-button">Logout</button>
</li>
</ul>
</nav>
)}
La navbar viene mostrata solo se l’utente è autenticato.
Mostra le sezioni in base al ruolo dell’utente (utente normale o admin).
Se l’utente è admin, compaiono le voci Admin Dashboard, Configuration e Download Backup.
Il pulsante Logout permette di uscire dall’applicazione.
Gestione delle rotte con React Router
<Routes>
<Route path="/" element={isAuthenticated ? <ThumbnailGrid /> : <Navigate to="/login" />} />
<Route path="/videos" element={isAuthenticated ? <VideoGrid /> : <Navigate to="/login" />} />
{isAdmin && (
<>
<Route path="/configuration" element={isAuthenticated ? (
<iframe src={`${config.API_BASE_URL}/static/config_manager.html`} style={{ width: "100%", height: "80vh", border: "none" }} title="Configuration" />
) : (<Navigate to="/login" />)} />
<Route path="/admin-dashboard" element={isAuthenticated ? <AdminDashboard /> : <Navigate to="/login" />} />
</>
)}
<Route path="/user-settings" element={isAuthenticated ? <UserSettings /> : <Navigate to="/login" />} />
<Route path="/login" element={isAuthenticated ? <Navigate to="/" /> : <LoginPage onLogin={handleLogin} />} />
<Route path="/statistics" element={isAuthenticated ? <StatisticsPage /> : <Navigate to="/login" />} />
</Routes>
Le pagine sono accessibili solo agli utenti autenticati.
Se un utente non è autenticato, viene reindirizzato alla pagina di login.
L’ admin può accedere alla dashboard e alla configurazione.
File: config.js – Configurazione dell’API
const config = {
API_BASE_URL: "http://192.168.1.190:8000", // URL backend
};
export default config;
Il file config.js ha un unico scopo: definire la base URL del backend dell’applicazione, centralizzando così la configurazione delle API per facilitarne la modifica in caso di cambiamento dell’indirizzo del server.
- API_BASE_URL → Contiene l’indirizzo IP e la porta del backend FastAPI (in questo caso, http://192.168.1.190:8000).
- Export di default → Permette di importare la configurazione negli altri file dell’applicazione React.
Vantaggi di centralizzare la configurazione
✅ Facilità di modifica → se l’IP del backend cambia, basta aggiornare questo file senza dover modificare ogni singolo file del frontend.
✅ Evita duplicazione di codice → qualsiasi richiesta API può fare riferimento a config.API_BASE_URL invece di scrivere direttamente l’URL ovunque.
✅ Maggiore leggibilità e manutenibilità → il codice risulta più pulito e organizzato.
🔹 Esempio di utilizzo:
Nei componenti React, invece di scrivere manualmente l’URL, si usa:
import config from './config';
fetch(`${config.API_BASE_URL}/photos`)
.then(response => response.json())
.then(data => console.log(data));
In questo modo, tutte le chiamate API fanno riferimento all’URL centralizzato.
Vediamo ora i files nella cartella components.
AdminDashboard.jsx
Il componente AdminDashboard fornisce un’interfaccia per la gestione degli utenti dell’applicazione, permettendo all’amministratore di aggiungere, modificare ed eliminare utenti, oltre a gestire le proprie credenziali.
Importazione delle dipendenze
import React, { useState, useEffect } from "react";
import config from '../config';
import "../styles/AdminDashboard.css";
React e gli hook
useState e useEffect vengono importati per la gestione dello stato e degli effetti collaterali.
config.js viene importato per ottenere l’URL del backend.
AdminDashboard.css contiene lo stile specifico per il componente.
Dichiarazione dello stato locale
const [users, setUsers] = useState([]);
const [newUser, setNewUser] = useState({ username: "", password: "" });
const [currentAdmin, setCurrentAdmin] = useState({
currentPassword: "",
newUsername: "",
newPassword: "",
});
const [editingUser, setEditingUser] = useState(null);
const [editUser, setEditUser] = useState({ username: "", password: "" });
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [language, setLanguage] = useState("it");
users contiene la lista degli utenti.
newUser è un oggetto che contiene username e password del nuovo utente.
currentAdmin gestisce le credenziali dell’amministratore per aggiornare i propri dati.
editingUser contiene l’ID dell’utente che si sta modificando.
editUser memorizza le informazioni aggiornate dell’utente in modifica.
error e success vengono usati per mostrare messaggi di errore o successo.
language gestisce la lingua dell’interfaccia (it per italiano, en per inglese).
Gestione della traduzione
const t = (key) => translations[language][key];
const translateBackendMessage = (message) => {
const translated =
translations[language].backend[message] || message;
return translated;
};
t() recupera le traduzioni in base alla lingua selezionata.
translateBackendMessage() traduce i messaggi di errore provenienti dal backend.
Recupero della lista utenti
const fetchUsers = async () => {
try {
const response = await fetch(`${config.API_BASE_URL}/users`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (!response.ok) {
throw new Error(t("errorFetch"));
}
const data = await response.json();
setUsers(data);
} catch (err) {
setError(translateBackendMessage(err.message) || t("errorFetch"));
}
};
Questa funzione:
- Effettua una richiesta GET all’endpoint /users per ottenere l’elenco utenti.
- Se la richiesta fallisce, genera un errore tradotto.
La funzione viene chiamata al montaggio del componente con useEffect():
useEffect(() => {
fetchUsers();
}, []);
Aggiunta di un nuovo utente
const handleAddUser = async (e) => {
e.preventDefault();
setError("");
setSuccess("");
try {
const response = await fetch(`${config.API_BASE_URL}/users/add`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(newUser),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(translateBackendMessage(errorData.detail));
}
setSuccess(t("successAdd"));
setNewUser({ username: "", password: "" });
fetchUsers();
} catch (err) {
setError(translateBackendMessage(err.message));
}
};
Invia una richiesta POST a /users/add.
Se ha successo, aggiorna la lista utenti e mostra un messaggio di conferma.
Se fallisce, mostra un messaggio di errore.
Eliminazione di un utente
const handleDeleteUser = async (username) => {
try {
const response = await fetch(`${config.API_BASE_URL}/users/delete`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({ username }),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(translateBackendMessage(errorData.detail));
}
setSuccess(t("successDelete"));
setUsers((prev) => prev.filter((user) => user.username !== username));
} catch (err) {
setError(translateBackendMessage(err.message));
}
};
Invia una richiesta DELETE a /users/delete per eliminare un utente.
Se ha successo, aggiorna la lista utenti rimuovendo l’utente eliminato.
Modifica di un utente
const handleEditUser = (user) => {
setEditingUser(user.id);
setEditUser({ username: user.username, password: "" });
};
Memorizza l’utente in fase di modifica.
const handleSaveUser = async (e) => {
e.preventDefault();
setError("");
setSuccess("");
try {
const response = await fetch(
`${config.API_BASE_URL}/users/update/${editingUser}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
username: editUser.username,
password: editUser.password || undefined,
}),
}
);
if (!response.ok) {
const errorData = await response.json();
throw new Error(translateBackendMessage(errorData.detail));
}
setSuccess(t("successUpdate"));
setEditingUser(null);
fetchUsers();
} catch (err) {
setError(translateBackendMessage(err.message));
}
};
Invia una richiesta PUT a /users/update/{user_id} per aggiornare i dati dell’utente.
Aggiornamento delle credenziali dell’admin
const handleUpdateAdmin = async (e) => {
e.preventDefault();
setError("");
setSuccess("");
try {
const response = await fetch(`${config.API_BASE_URL}/admin/update`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
current_password: currentAdmin.currentPassword,
new_username: currentAdmin.newUsername || undefined,
new_password: currentAdmin.newPassword || undefined,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(translateBackendMessage(errorData.detail));
}
setSuccess(t("successUpdate"));
setCurrentAdmin({
currentPassword: "",
newUsername: "",
newPassword: "",
});
} catch (err) {
setError(translateBackendMessage(err.message));
}
};
Permette all’admin di modificare username e password tramite /admin/update.
Rendering del componente
Il componente AdminDashboard visualizza:
- Un selettore della lingua (it/en).
- Lista utenti con pulsanti di eliminazione e modifica.
- Form di modifica per l’utente selezionato.
- Form per aggiungere un nuovo utente.
- Form per aggiornare l’admin.
Esportazione del componente
export default AdminDashboard;
Permette di importarlo in altri file.
LoginPage.jsx
Il componente LoginPage è responsabile della gestione del login dell’utente, consentendo l’inserimento delle credenziali e la verifica con il backend.
Importazione delle dipendenze
import React, { useState } from "react";
import config from '../config';
import "../styles/LoginPage.css";
React e useState: importati per la gestione dello stato locale.
config.js: contiene l’URL del backend per l’autenticazione.
LoginPage.css: stile del modulo di login.
Gestione dello stato locale
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
username e password contengono le credenziali inserite dall’utente.
error è usato per mostrare un messaggio di errore in caso di login fallito.
Funzione di gestione del login
const handleSubmit = async (e) => {
e.preventDefault();
setError("");
try {
const response = await fetch(`${config.API_BASE_URL}/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
throw new Error("Login failed");
}
const data = await response.json();
onLogin(data.access_token); // Aggiorna lo stato di autenticazione
} catch (err) {
setError("Invalid username or password");
}
};
Effettua una richiesta POST a /login per autenticare l’utente.
Se il login fallisce, viene mostrato un messaggio di errore.
Se ha successo, il token viene passato alla funzione onLogin() per gestire lo stato di autenticazione.
Rendering del modulo di login
return (
<div className="login-page">
<div className="login-container">
<h2>Login</h2>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="username">Username:</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
</div>
<div className="form-group">
<label htmlFor="password">Password:</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">Login</button>
</form>
{error && <p className="error-message">{error}</p>}
</div>
</div>
);
Modulo di login con due campi (username e password).
Bottone di invio che chiama handleSubmit().
Messaggio di errore visualizzato se il login fallisce.
Esportazione del componente
export default LoginPage;
Permette di importarlo in altri file.
StatisticsPage.jsx
Il componente StatisticsPage mostra statistiche sui file multimediali e sulle risorse di sistema tramite tabelle e grafici interattivi.
Importazione delle dipendenze
import React, { useState, useEffect } from "react";
import config from '../config';
import "../styles/StatisticsPage.css";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
ArcElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import { Bar, Doughnut } from "react-chartjs-2";
React e useState, useEffect: gestione dello stato e dell’aggiornamento dei dati.
config.js: contiene l’URL del backend per recuperare le statistiche.
StatisticsPage.css: foglio di stile per il layout.
Chart.js: libreria per la generazione di grafici a barre e a torta.
Registrazione degli elementi di Chart.js
ChartJS.register(CategoryScale, LinearScale, BarElement, ArcElement, Title, Tooltip, Legend);
Registra gli elementi necessari per il rendering dei grafici.
Gestione dello stato
const [mediaStats, setMediaStats] = useState(null);
const [systemStats, setSystemStats] = useState(null);
const [language, setLanguage] = useState("en");
mediaStats e systemStats contengono rispettivamente le statistiche sui file multimediali e sulle risorse di sistema.
language permette di cambiare lingua tra italiano e inglese.
Traduzioni dei testi
const translations = {
en: {
title: "Statistics",
mediaStatsTitle: "Media Statistics",
systemStatsTitle: "System Statistics",
totalPhotos: "Total Photos",
photosLastMonth: "Photos Last Month",
totalPhotoSize: "Total Photo Size",
totalVideos: "Total Videos",
videosLastMonth: "Videos Last Month",
totalVideoSize: "Total Video Size",
totalVideoDuration: "Total Video Duration",
cpuUsage: "CPU Usage",
memoryUsage: "Memory Usage",
diskUsage: "Disk Usage",
swapUsage: "Swap Usage",
minutes: "minutes",
},
it: {
title: "Statistiche",
mediaStatsTitle: "Statistiche Media",
systemStatsTitle: "Statistiche Sistema",
totalPhotos: "Foto Totali",
photosLastMonth: "Foto Ultimo Mese",
totalPhotoSize: "Dimensione Totale Foto",
totalVideos: "Video Totali",
videosLastMonth: "Video Ultimo Mese",
totalVideoSize: "Dimensione Totale Video",
totalVideoDuration: "Durata Totale Video",
cpuUsage: "Utilizzo CPU",
memoryUsage: "Utilizzo Memoria",
diskUsage: "Utilizzo Disco",
swapUsage: "Utilizzo Swap",
minutes: "minuti",
},
};
const t = (key) => translations[language][key];
Le stringhe vengono tradotte dinamicamente in base alla lingua selezionata.
Recupero delle statistiche dal backend
useEffect(() => {
const fetchStats = async () => {
try {
const mediaResponse = await fetch(`${config.API_BASE_URL}/media-stats`);
const systemResponse = await fetch(`${config.API_BASE_URL}/system-stats`);
setMediaStats(await mediaResponse.json());
setSystemStats(await systemResponse.json());
} catch (error) {
console.error("Error fetching statistics:", error);
}
};
fetchStats();
const interval = setInterval(() => { fetchStats(); }, 10000);
return () => clearInterval(interval);
}, []);
Effettua richieste API ogni 10 secondi per aggiornare i dati.
setMediaStats e setSystemStats aggiornano i dati recuperati.
Interfaccia utente
if (!mediaStats || !systemStats) {
return <p>Loading statistics...</p>;
}
Se i dati non sono ancora stati caricati, mostra un messaggio di attesa.
Pulsanti per la selezione della lingua
<div className="language-switch">
<button onClick={() => setLanguage("it")}>
<img src="https://upload.wikimedia.org/wikipedia/commons/0/03/Flag_of_Italy.svg" alt="Italian" />
</button>
<button onClick={() => setLanguage("en")}>
<img src="https://upload.wikimedia.org/wikipedia/commons/a/a5/Flag_of_the_United_Kingdom_%281-2%29.svg" alt="English" />
</button>
</div>
Cambia lingua tramite un set di pulsanti con le bandiere corrispondenti.
Tabella delle statistiche multimediali
<h3>{t("mediaStatsTitle")}</h3>
<table className="media-stats-table">
<tbody>
<tr>
<td colSpan="2" className="section-header">{t("totalPhotos")}</td>
</tr>
<tr>
<td>{t("totalPhotos")}</td>
<td>{mediaStats.photos.total_count}</td>
</tr>
<tr>
<td>{t("photosLastMonth")}</td>
<td>{mediaStats.photos.last_month_count}</td>
</tr>
<tr>
<td>{t("totalPhotoSize")}</td>
<td>{mediaStats.photos.total_size_mb.toFixed(2)} MB</td>
</tr>
</tbody>
</table>
Mostra il numero totale di foto e video, il loro peso e le statistiche dell’ultimo mese.
Grafico a barre sulle statistiche multimediali
<Bar
data={{
labels: [t("totalPhotos"), t("totalVideos")],
datasets: [
{
label: t("mediaStatsTitle"),
backgroundColor: ["#36A2EB", "#FF6384"],
data: [mediaStats.photos.total_count, mediaStats.videos.total_count],
},
],
}}
options={{ maintainAspectRatio: true }}
/>
Mostra il totale di foto e video in un grafico a barre.
Grafico a torta per le risorse di sistema
<Doughnut
data={{
labels: [t("cpuUsage"), t("memoryUsage"), t("diskUsage"), t("swapUsage")],
datasets: [
{
label: "System Usage",
data: [
systemStats.cpu_usage_percent,
systemStats.memory.used_mb / systemStats.memory.total_mb * 100,
systemStats.disk.used_gb / systemStats.disk.total_gb * 100,
systemStats.swap.used_mb / systemStats.swap.total_mb * 100,
],
backgroundColor: ["#FF6384", "#36A2EB", "#FFCE56", "#8E44AD"],
hoverOffset: 4,
},
],
}}
options={{
plugins: {
tooltip: {
callbacks: {
label: (tooltipItem) => {
const label = tooltipItem.label || "";
const value = tooltipItem.raw.toFixed(2);
return `${label}: ${value}%`;
},
},
},
},
}}
/>
Visualizza l’uso della CPU, della RAM, dello spazio su disco e dello swap in un grafico a torta.
Esportazione del componente
export default StatisticsPage;
Rende disponibile il componente per l’uso in altre parti del progetto.
ThumbnailGrid.jsx
Il componente ThumbnailGrid gestisce la visualizzazione delle miniature delle foto acquisite dal sistema di videosorveglianza. Consente inoltre la selezione della lingua, la gestione della paginazione e l’apertura di un popup con l’immagine ingrandita.
Importazione delle dipendenze
import React, { useEffect, useState } from "react";
import config from '../config';
import "../styles/ThumbnailGrid.css";
React e gli hook useState e useEffect: Permettono di gestire lo stato e l’aggiornamento delle immagini.
config.js: Contiene l’URL del backend per recuperare le foto.
ThumbnailGrid.css: File di stile per il layout delle miniature.
Gestione dello stato
const [images, setImages] = useState([]);
const [totalPages, setTotalPages] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [language, setLanguage] = useState("it");
const [selectedImage, setSelectedImage] = useState(null);
images: contiene l’elenco delle foto da visualizzare.
totalPages: numero totale di pagine disponibili.
currentPage: pagina attualmente visualizzata.
pageSize: numero di foto per pagina.
language: lingua attualmente selezionata.
selectedImage: immagine selezionata per l’anteprima ingrandita.
Traduzioni dinamiche
const translations = {
it: {
pageSizeLabel: "Miniature per pagina:",
sectionLabel: "Sezione foto",
paginationLabel: "Pagina {currentPage} di {totalPages}"
},
en: {
pageSizeLabel: "Photos per page:",
sectionLabel: "Photos section",
paginationLabel: "Page {currentPage} of {totalPages}"
}
};
const t = (key) => translations[language][key];
Traduzioni per italiano e inglese, utilizzate per la UI.
t() restituisce il valore della stringa tradotta in base alla lingua corrente.
Placeholder per immagini mancanti
const placeholderImage = "https://placehold.co/150x150?text=Not+Found";
Se una foto non viene caricata correttamente, viene mostrata un’immagine di placeholder.
Funzione per recuperare le foto paginate
const fetchPhotos = async (page, size) => {
try {
const response = await fetch(
`${config.API_BASE_URL}/photos/?page=${page}&page_size=${size}`
);
const data = await response.json();
setImages(data);
const countResponse = await fetch(
`${config.API_BASE_URL}/photos/count/?page_size=${size}`
);
const countData = await countResponse.json();
setTotalPages(countData.total_pages);
} catch (error) {
console.error("Errore durante il caricamento delle foto:", error);
}
};
Effettua due chiamate API:
- Recupera le immagini della pagina corrente.
- Recupera il numero totale di pagine disponibili.
I dati vengono salvati negli state images e totalPages.
Caricamento delle foto all’avvio e a ogni cambio di pagina
useEffect(() => {
fetchPhotos(currentPage, pageSize);
}, [currentPage, pageSize]);
Esegue fetchPhotos() quando cambia la pagina o la dimensione delle foto per pagina.
Selettore della lingua
<div className="language-switch">
<button className="btn btn-outline-primary" onClick={() => setLanguage("it")}>
<img src="https://upload.wikimedia.org/wikipedia/commons/0/03/Flag_of_Italy.svg" alt="Italian" />
</button>
<button className="btn btn-outline-primary" onClick={() => setLanguage("en")}>
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Flag_of_the_United_Kingdom_%281-2%29.svg/1920px-Flag_of_the_United_Kingdom_%281-2%29.svg.png" alt="English" />
</button>
</div>
Permette all’utente di selezionare la lingua dell’interfaccia tramite due pulsanti con le bandiere.
Selettore del numero di immagini per pagina
<div className="page-size-selector">
<label htmlFor="pageSize">{t("pageSizeLabel")}</label>
<select
id="pageSize"
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value));
setCurrentPage(1);
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
Permette all’utente di selezionare il numero di foto da visualizzare per pagina. Resetta la paginazione alla prima pagina dopo ogni modifica.
Visualizzazione delle immagini
<div className="thumbnail-grid">
{images.map((image) => (
<img
key={image.id}
src={`${config.API_BASE_URL}/download-thumbnail/${image.id}`}
alt={image.filename}
title={image.filename}
className="thumbnail"
onError={(e) => (e.target.src = placeholderImage)}
onClick={() => setSelectedImage(`${config.API_BASE_URL}/download/${image.id}`)}
/>
))}
</div>
Visualizza tutte le miniature delle immagini disponibili.
Se un’immagine non viene caricata, usa l’immagine di fallback.
Cliccando su una miniatura, viene aperta la versione ingrandita in un popup.
Gestione della paginazione
<div className="pagination">
<button
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
<
</button>
<span>
{t("paginationLabel")
.replace("{currentPage}", currentPage)
.replace("{totalPages}", totalPages)}
</span>
<button
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
>
>
</button>
</div>
I pulsanti di navigazione permettono di spostarsi tra le pagine delle immagini.
Il testo viene aggiornato dinamicamente tramite traduzioni.
Popup con immagine ingrandita
{selectedImage && (
<div className="popup">
<div className="popup-content">
<button className="close-popup" onClick={() => setSelectedImage(null)}>
×
</button>
<img src={selectedImage} alt="Selected" />
</div>
</div>
)}
Quando l’utente clicca su un’immagine, viene aperto un popup che mostra la versione ingrandita.
Il popup può essere chiuso cliccando sulla “X”.
Esportazione del componente
export default ThumbnailGrid;
Permette di importare ThumbnailGrid in altri file dell’applicazione.
UserSettings.jsx
Il componente UserSettings permette agli utenti autenticati di cambiare la propria password. Include il supporto per la localizzazione in italiano e inglese e gestisce le richieste API per aggiornare la password nel backend.
Importazione delle dipendenze
import React, { useState } from "react";
import config from '../config';
import "../styles/UserSettings.css";
React e gli hook useState: per gestire lo stato dei dati del modulo.
config.js: contiene l’URL del backend per le chiamate API.
UserSettings.css: file di stile per la formattazione del componente.
Gestione dello stato
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [error, setError] = useState("");
const [success, setSuccess] = useState("");
const [language, setLanguage] = useState("it");
currentPassword: memorizza la password attuale dell’utente.
newPassword: memorizza la nuova password che l’utente vuole impostare.
error: contiene eventuali messaggi di errore ricevuti dal backend.
success: contiene messaggi di conferma per l’aggiornamento della password.
language: tiene traccia della lingua selezionata dall’utente.
Traduzioni dinamiche
const translations = {
it: {
title: "Impostazioni Utente",
currentPassword: "Password Attuale:",
newPassword: "Nuova Password:",
changePasswordButton: "Cambia Password",
"Password attuale errata": "Password attuale errata",
"Password aggiornata con successo": "Password aggiornata con successo",
},
en: {
title: "User Settings",
currentPassword: "Current Password:",
newPassword: "New Password:",
changePasswordButton: "Change Password",
"Password attuale errata": "Incorrect current password",
"Password aggiornata con successo": "Password successfully updated",
},
};
const t = (message) => translations[language]?.[message] || message;
Definisce i testi dell’interfaccia utente in italiano e inglese.
La funzione t(message) restituisce il messaggio tradotto in base alla lingua selezionata.
Funzione per la gestione del cambio password
const handleChangePassword = async (e) => {
e.preventDefault();
setError("");
setSuccess("");
const token = localStorage.getItem("token");
try {
if (!token) {
throw new Error("Nessun token trovato. Effettua nuovamente il login.");
}
const response = await fetch(`${config.API_BASE_URL}/users/change-password`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword,
}),
});
if (!response.ok) {
const errorData = await response.json();
throw new Error(t(errorData.detail));
}
const data = await response.json();
setSuccess(t(data.info));
setCurrentPassword("");
setNewPassword("");
} catch (err) {
setError(err.message);
}
};
Recupera il token dalla memoria locale per l’autenticazione. Invia una richiesta POST al backend con la password attuale e la nuova password. Gestisce errori e successi:
- Se la risposta è negativa, mostra un messaggio di errore tradotto.
- Se la risposta è positiva, pulisce il modulo e mostra un messaggio di conferma.
Interfaccia utente e selezione lingua
<div className="language-switch">
<button onClick={() => setLanguage("it")}>
<img
src="https://upload.wikimedia.org/wikipedia/commons/0/03/Flag_of_Italy.svg"
alt="Italian"
style={{ width: "30px", height: "20px" }}
/>
</button>
<button onClick={() => setLanguage("en")}>
<img
src="https://upload.wikimedia.org/wikipedia/commons/a/a5/Flag_of_the_United_Kingdom_%281-2%29.svg"
alt="English"
style={{ width: "30px", height: "20px" }}
/>
</button>
</div>
Permette all’utente di cambiare lingua cliccando su un’icona con bandiera.
Struttura del modulo
<h2>{t("title")}</h2>
{error && <p className="error-message">{error}</p>}
{success && <p className="success-message">{success}</p>}
<form onSubmit={handleChangePassword}>
<label>
{t("currentPassword")}
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
/>
</label>
<label>
{t("newPassword")}
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
/>
</label>
<button type="submit">{t("changePasswordButton")}</button>
</form>
Mostra il titolo della pagina in base alla lingua selezionata.
Visualizza eventuali messaggi di errore o successo.
Contiene il form con i campi per la password attuale e quella nuova.
Pulsante “Cambia Password” invia il modulo per l’aggiornamento.
Esportazione del componente
export default UserSettings;
Permette di importare UserSettings in altri file dell’applicazione.
VideoGrid.jsx
È praticamente il gemello della ThumbnailGrid.jsx, solo che anziché gestire le foto gestisce i video. La logica di impaginazione, il selettore del numero di elementi per pagina e il sistema di popup per la visualizzazione sono identici, cambia solo il tipo di contenuto.
import React, { useEffect, useState } from "react";
import config from '../config';
import "../styles/VideoGrid.css";
function VideoGrid() {
const [videos, setVideos] = useState([]); // Current videos
const [totalPages, setTotalPages] = useState(0); // Total pages
const [currentPage, setCurrentPage] = useState(1); // Current page
const [pageSize, setPageSize] = useState(10); // Number of videos per page
const [language, setLanguage] = useState("it"); // Selected language
const [popupVideo, setPopupVideo] = useState(null); // Video to show in popup
const translations = {
it: {
pageSizeLabel: "Video per pagina:",
sectionLabel: "Sezione video",
paginationLabel: "Pagina {currentPage} di {totalPages}",
},
en: {
pageSizeLabel: "Videos per page:",
sectionLabel: "Video section",
paginationLabel: "Page {currentPage} of {totalPages}",
},
};
const t = (key) => translations[language][key];
// Function to get paginated videos
const fetchVideos = async (page, size) => {
try {
const response = await fetch(
`${config.API_BASE_URL}/videos/?page=${page}&page_size=${size}`
);
const data = await response.json();
setVideos(data); // Update videos
const countResponse = await fetch(
`${config.API_BASE_URL}/videos/count/?page_size=${size}`
);
const countData = await countResponse.json();
setTotalPages(countData.total_pages); // Use total_pages from backend
} catch (error) {
console.error("Errore durante il caricamento dei video:", error);
}
};
// Load videos on startup and when page or pageSize changes
useEffect(() => {
fetchVideos(currentPage, pageSize);
}, [currentPage, pageSize]);
return (
<div>
<div className="language-switch">
<button className="btn btn-outline-primary" onClick={() => setLanguage("it")}>
<img
src="https://upload.wikimedia.org/wikipedia/commons/0/03/Flag_of_Italy.svg"
alt="Italian"
style={{ width: "30px", height: "20px" }}
/>
</button>
<button className="btn btn-outline-primary" onClick={() => setLanguage("en")}>
<img
src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a5/Flag_of_the_United_Kingdom_%281-2%29.svg/1920px-Flag_of_the_United_Kingdom_%281-2%29.svg.png"
alt="English"
style={{ width: "30px", height: "20px" }}
/>
</button>
</div>
<h1>{t("sectionLabel")}</h1>
{/* Selecting the number of videos per page */}
<div className="page-size-selector">
<label htmlFor="pageSize">{t("pageSizeLabel")}</label>
<select
id="pageSize"
value={pageSize}
onChange={(e) => {
setPageSize(Number(e.target.value)); // Update the number of videos per page
setCurrentPage(1); // Reset to first page
}}
>
<option value={5}>5</option>
<option value={10}>10</option>
<option value={20}>20</option>
<option value={50}>50</option>
</select>
</div>
<div className="video-grid">
{videos.map((video) => (
<div key={video.id} className="video-thumbnail">
<img
src={`${config.API_BASE_URL}/videos/thumbnails/${video.filename}.jpg`}
alt={video.filename}
title={video.filename}
onClick={() => setPopupVideo(video)}
/>
</div>
))}
</div>
{/* Pagination controls */}
<div className="pagination">
<button
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
>
<
</button>
<span>
{t("paginationLabel")
.replace("{currentPage}", currentPage)
.replace("{totalPages}", totalPages)}
</span>
<button
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
>
>
</button>
</div>
{/* Popup for video playback */}
{popupVideo && (
<div className="popup" onClick={() => setPopupVideo(null)}>
<div className="popup-content" onClick={(e) => e.stopPropagation()}>
<button className="close-popup" onClick={() => setPopupVideo(null)}>
×
</button>
<video controls>
<source
src={`${config.API_BASE_URL}/videos/download/${popupVideo.id}`}
type="video/mp4"
/>
Your browser does not support the video tag.
</video>
<p>{popupVideo.filename}</p>
</div>
</div>
)}
</div>
);
}
export default VideoGrid;
Vediamo solo le differenze chiave rispetto a ThumbnailGrid.jsx:
- Gestione dei video invece delle immagini
- Invece di foto, la pagina carica i video dal backend tramite l’endpoint videos/.
- Il codice per ottenere il numero totale di pagine e la gestione della paginazione funziona nello stesso modo.
- Miniature dei video
- Usa ${config.API_BASE_URL}/videos/thumbnails/${video.filename}.jpg per recuperare l’anteprima di ogni video, mentre in ThumbnailGrid.jsx viene usato l’endpoint per scaricare le miniature delle foto.
- Popup per la riproduzione dei video
- Quando un utente clicca su una miniatura, si apre un popup con un elemento <video> che permette la riproduzione diretta.
- La sorgente del video è caricata da ${config.API_BASE_URL}/videos/download/${popupVideo.id}, quindi il backend deve fornire il file MP4.
- Struttura del grid leggermente diversa
- In ThumbnailGrid.jsx ogni elemento è una <img>, mentre qui ogni elemento è una <div> con un’<img> che funge da anteprima, e un click apre il popup del video.
config_manager.html
Il file config_manager.html è un’interfaccia web che permette all’amministratore di gestire e modificare la configurazione del sistema direttamente dal browser.
Questa pagina consente di aggiornare parametri sia di config.ini che di motion.conf, due file chiave per il funzionamento della piattaforma di videosorveglianza.
L’interfaccia fornisce:
- Campi di input per modificare i valori di configurazione.
- Checkbox e pulsanti di selezione per attivare/disattivare funzioni specifiche.
- Sezione dedicata a motion.conf, per configurare il comportamento della rilevazione di movimento.
- Salvataggio delle modifiche via API POST per aggiornare i file di configurazione.
- Supporto per più lingue (italiano e inglese), grazie a un sistema di traduzioni.
- Pulsante di riavvio del servizio, per applicare le modifiche riavviando l’intero sistema (videosurveillance.service).
Inclusione delle librerie e stile della pagina
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.1.3/lux/bootstrap.min.css">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet">
La pagina utilizza Bootstrap per la formattazione e Bootstrap Icons per alcune icone di gestione.
Viene inoltre applicato uno stile personalizzato per migliorare l’usabilità dell’interfaccia.
Switch per il cambio lingua
<div class="language-switch">
<button class="btn btn-outline-primary" onclick="setLanguage('it')">
<img src="https://upload.wikimedia.org/wikipedia/commons/0/03/Flag_of_Italy.svg" alt="Italian">
</button>
<button class="btn btn-outline-primary" onclick="setLanguage('en')">
<img src="https://upload.wikimedia.org/wikipedia/commons/a/a5/Flag_of_the_United_Kingdom_%281-2%29.svg" alt="English">
</button>
</div>
Questa sezione permette all’utente di cambiare la lingua dell’interfaccia tra italiano e inglese, aggiornando dinamicamente i testi.
Gestione di config.ini
Il file config.ini contiene vari parametri operativi. Questa sezione fornisce campi di input per modificarli direttamente.
Esempio: Modifica del ritardo di inizializzazione
<label for="initDelay" class="form-label">Initial Delay (seconds):</label>
<input type="number" class="form-control" id="initDelay" placeholder="Initial delay to start monitoring photos.">
L’utente può modificare il ritardo iniziale per l’avvio del monitoraggio delle foto.
Lo stesso approccio viene utilizzato per:
- Intervallo di elaborazione delle immagini.
- Numero massimo di foto/video da conservare.
- Dimensione dei batch per l’upload.
Gestione delle notifiche (Pushover, Telegram, Gmail)
Per ogni sistema di notifica, la pagina fornisce:
- Checkbox per abilitare/disabilitare il servizio.
- Campi di input per API token, user key e altre credenziali.
- Pulsante per mostrare/nascondere le credenziali (per sicurezza).
Esempio: Attivazione delle notifiche Telegram
<input type="checkbox" class="form-check-input" id="telegramEnabled">
<label for="telegramEnabled" class="form-check-label">Enable or disable notifications via Telegram.</label>
Se attivato, l’utente può inserire il token API e il chat ID per ricevere notifiche.
Gestione di motion.conf
Questa sezione permette di modificare il comportamento del sistema di rilevazione del movimento.
Selezione del tipo di output
<select class="form-select" id="outputMode">
<option value="photos">PHOTOS</option>
<option value="videos">VIDEOS</option>
</select>
L’utente può scegliere se salvare solo foto o solo video.
Altri parametri configurabili
- Soglia di rilevamento del movimento (threshold)
- Numero minimo di frame per generare un evento (minimum_motion_frames)
- Livello di rumore accettabile (noise_level)
- Filtro di riduzione del rumore (despeckle_filter)
- Regolazione automatica della luminosità (auto_brightness)
Salvataggio della configurazione
Quando l’utente modifica i parametri, può inviare le nuove configurazioni al server tramite un’API REST.
fetch("/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
general: configIni.general,
notification: configIni.notification,
motion: { motion_conf_content: motionConfig }
})
})
Le modifiche vengono inviate al backend, che aggiorna i file config.ini e motion.conf.
Riavvio del servizio
Per applicare le nuove impostazioni, la pagina fornisce un pulsante per applicare le modifiche riavviando l’intero sistema (videosurveillance.service).
fetch("/restart-service", {
method: "POST",
headers: { "Content-Type": "application/json" }
})
Dopo il riavvio, il sistema inizierà ad operare con i nuovi parametri.
Il file config_manager.html fornisce un’interfaccia grafica avanzata per la gestione del sistema, eliminando la necessità di modificare manualmente i file di configurazione.
Grazie al supporto per più lingue e alla possibilità di modificare sia config.ini che motion.conf, permette un controllo completo del sistema direttamente dal browser.
Descrizione degli script Docker, come installano BE e FE
Il sistema di videosorveglianza è containerizzato utilizzando Docker e Docker Compose per garantire modularità, portabilità e una gestione più semplice delle dipendenze.
L’architettura si basa su tre file principali:
- Dockerfile del Backend: configura e avvia il server FastAPI con Motion e il monitoraggio.
- Dockerfile del Frontend: costruisce e serve l’applicazione React con Nginx.
- docker-compose.yml: orchestra il backend e il frontend, gestendo volumi, reti e configurazioni.
Obiettivi della containerizzazione
✔️ Isolamento: ogni componente (backend, frontend) gira in un container separato.
✔️ Portabilità: il sistema può essere eseguito su qualsiasi macchina con Docker installato.
✔️ Scalabilità: possibilità di espandere il sistema senza problemi di dipendenze.
✔️ Facilità di deploy: basta un comando per avviare tutto.
Il file docker-compose.yml
Il file docker-compose.yml è il cuore dell’orchestrazione del sistema di videosorveglianza. Definisce due servizi:
- backend (FastAPI con Motion)
- frontend (React servito con Nginx)
Struttura generale
version: "3.3"
services:
backend:
...
frontend:
...
version: “3.3“: specifica la versione di Docker Compose in uso.
services: definisce i container che verranno eseguiti.
Configurazione del Backend
backend:
build:
context: .
container_name: videosurveillance_backend
ports:
- "8000:8000"
volumes:
- .:/app
- ./photos:/app/photos
- ./videos:/app/videos
- ./data:/app/data
- ./config.ini:/app/config.ini
- ./motion.conf:/etc/motion/motion.conf
- /var/run/docker.sock:/var/run/docker.sock
- /etc/localtime:/etc/localtime:ro
- /etc/timezone:/etc/timezone:ro
environment:
- PYTHONUNBUFFERED=1
devices:
- "/dev/video0:/dev/video0"
network_mode: "host"
🔹 build:
- Indica che l’immagine Docker del backend verrà costruita dal Dockerfile nella directory corrente (
.
).
🔹 container_name:
- Assegna un nome statico al container (videosurveillance_backend), utile per riferirsi ad esso nei comandi e nei log.
🔹 ports:
- Mappa la porta 8000 del container sulla porta 8000 dell’host → permette l’accesso all’API FastAPI.
🔹 volumes:
- Permette la persistenza dei dati tra il container e l’host:
- ./photos:/app/photos → mantiene le foto salvate anche dopo il riavvio del container.
- ./videos:/app/videos → stessa cosa per i video.
- ./data:/app/data → mantiene il database SQLite.
- ./config.ini:/app/config.ini → mantiene le impostazioni di configurazione.
- ./motion.conf:/etc/motion/motion.conf → permette di modificare la configurazione di Motion senza ricreare il container.
- /var/run/docker.sock:/var/run/docker.sock → permette al container di controllare altri container, utile per il riavvio automatico.
/
etc/localtime:/etc/localtime:ro e /etc/timezone:/etc/timezone:ro → sincronizza il fuso orario con quello dell’host.
🔹 environment:
- PYTHONUNBUFFERED=1 → disabilita il buffering dello stdout/stderr di Python per una visualizzazione immediata dei log.
🔹 devices:
- “/dev/video0:/dev/video0” → collega la webcam dell’host al container, permettendo a Motion di accedere alla videocamera.
🔹 network_mode: “host”
- Permette al container di usare direttamente la rete dell’host, necessario per comunicare con la videocamera.
Configurazione del Frontend
frontend:
build:
context: ./frontend
container_name: videosurveillance_frontend
ports:
- "3000:80"
volumes:
- ./frontend/src:/app/src
- ./frontend/public:/app/public
command: >
nginx -g 'daemon off;'
🔹 build:
- Il frontend viene costruito dalla cartella ./frontend, che contiene il relativo Dockerfile.
🔹 container_name:
- Assegna un nome statico al container (videosurveillance_frontend).
🔹 ports:
- Mappa la porta 80 del container sulla porta 3000 dell’host → il frontend sarà accessibile via http://IPRASPBERRY:3000.
🔹 volumes:
- Permette di aggiornare il codice frontend senza ricostruire il container:
- ./frontend/src:/app/src → sincronizza il codice React.
- ./frontend/public:/app/public → sincronizza le risorse pubbliche.
🔹 command:
- Avvia Nginx in modalità “foreground” (daemon off;), così il container rimane attivo.
Il Dockerfile per il Backend
Il Dockerfile del backend definisce l’ambiente necessario per eseguire il server FastAPI, il software Motion per la registrazione video e il monitoraggio dei file.
Base Image e impostazioni iniziali
# Use the official Python image
FROM python:3.9-slim
Usa l’immagine ufficiale Python 3.9 slim, una versione leggera che include solo i pacchetti essenziali.
# Set the frontend to be non-interactive to avoid warning messages during installation
ENV DEBIAN_FRONTEND=noninteractive
ENV TZ=Europe/Rome
DEBIAN_FRONTEND=noninteractive: evita richieste interattive durante l’installazione dei pacchetti.
TZ=Europe/Rome: imposta il fuso orario dell’ambiente per sincronizzare log ed eventi.
Impostazioni utente e permessi
# Run as root user to ensure full permissions
USER root
# Add root user to Docker group
RUN groupadd -f docker && usermod -aG docker root
Il container viene eseguito come utente root, necessario per controllare Motion e Docker.
Aggiunge root al gruppo docker, permettendogli di controllare altri container (utile per il riavvio automatico).
Creazione delle cartelle necessarie
# Create destination folders for images and videos
RUN mkdir -p /app/photos/motion /app/videos/motion
Crea le cartelle in cui Motion salverà foto e video prima che vengano processati. Questo garantisce che, anche alla prima esecuzione, i percorsi esistano già.
Installazione delle dipendenze di sistema
# Installs build tools and system dependencies, including those for motion, pillow, and package management
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
g++ \
cmake \
ninja-build \
apt-utils \
gcc \
python3-dev \
libjpeg-dev \
zlib1g-dev \
libpng-dev \
libffi-dev \
motion \
docker.io \
ffmpeg \
curl \
&& rm -rf /var/lib/apt/lists/*
Installa Motion, FFmpeg, e altri strumenti di compilazione richiesti da alcune librerie Python.
motion: software per il rilevamento di movimento e la registrazione video.
ffmpeg: necessario per elaborare video e generare miniature.
docker.io: permette al container di eseguire comandi Docker.
rm -rf /var/lib/apt/lists/* : riduce lo spazio occupato cancellando i file temporanei di apt-get.
Installazione delle dipendenze Python
# Set working directory in container
WORKDIR /app
# Copy the requirements.txt file to your working directory
COPY requirements.txt .
# Update pip before installing dependencies
RUN pip install --upgrade pip setuptools wheel
# Install precompiled numpy before other dependencies
RUN pip install --no-cache-dir numpy==2.0.2
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir PyJWT
RUN pip install apscheduler
# Install psutil for system resource monitoring
RUN pip install --no-cache-dir psutil
# List Python dependencies
RUN pip list
# Install PIL separately if not already present
RUN pip install pillow
Imposta la cartella di lavoro in /app.
Installa tutte le dipendenze dal file requirements.txt.
Installa Numpy 2.0.2 prima degli altri pacchetti, per evitare problemi di compatibilità.
Installa PyJWT per la gestione dell’autenticazione JWT.
Installa APScheduler per la gestione delle operazioni periodiche (es. backup).
Installa Psutil per il monitoraggio delle risorse di sistema.Verifica la lista delle dipendenze installate con pip list.
Copia del codice nel container
# Copy all the rest of the project files into the working directory
COPY . .
# Copy the contents of the static folder into the container
COPY ./static /app/static
Copia tutto il codice sorgente dentro il container.
Assicura che la cartella static (contenente file come CSS, immagini, ecc.) sia presente.
Apertura della porta e avvio del servizio
# Expose port 8000 (FastAPI uses this port by default)
EXPOSE 8000
Espone la porta 8000, necessaria per accedere al server FastAPI.
# Command to start Motion, FastAPI app and monitoring script in parallel
CMD ["sh", "-c", "motion & uvicorn main:app --host 0.0.0.0 --port 8000 & python3 /app/motion_monitor.py"]
Avvia Motion, il server FastAPI e lo script di monitoraggio in parallelo.
- motion &: avvia il software di rilevamento del movimento.
- uvicorn main:app –host 0.0.0.0 –port 8000 &: avvia il server FastAPI per le API.
- python3 /app/motion_monitor.py: avvia il monitoraggio di foto/video.
Il Dockerfile per il Frontend
Il Dockerfile del frontend è responsabile della costruzione e della distribuzione dell’applicazione React. Utilizza Node.js per compilare il codice e Nginx per servire l’applicazione in produzione.
Creazione dell’ambiente di build
# Use Node.js image to build the app
FROM node:18-alpine AS build
Usa Node.js 18 Alpine, una versione leggera di Node.js, per ridurre il peso dell’immagine.
Definisce un “build stage” chiamato build, che verrà usato per compilare l’app React.
# Check npm
RUN npm --version && echo "npm is installed correctly"
Verifica che npm (Node Package Manager) sia installato e funzionante. Stampa un messaggio di conferma nel log.
Installazione delle dipendenze
# Add Git (required for some dependencies)
RUN apk add --no-cache git
# Set the working directory
WORKDIR /app
# Automatically creates package.json and package-lock.json
RUN npm init -y
# Add the "build" script to the package.json
RUN sed -i 's/"scripts": {/"scripts": {"build": "vite build",/g' package.json
# Install basic dependencies (vite, react, react-dom)
RUN npm install [email protected] [email protected] [email protected]
RUN npm install --save-dev vite
# Install Chart.js and the React wrapper
RUN npm install chart.js react-chartjs-2
Installa Git, necessario per alcune dipendenze NPM usando –no-cache per evitare di salvare file temporanei, riducendo il peso del container.
Imposta /app come directory di lavoro per tutti i comandi successivi.
Crea automaticamente un package.json, il file di configurazione dei pacchetti NPM. Il flag -y accetta tutte le impostazioni predefinite.
Modifica il package.json per aggiungere il comando “build”:
“vite build”. Questo permette di usare npm run build per compilare il progetto con Vite.
Installa le librerie base di React (react, react-dom, react-router-dom). Installa Vite, un bundler veloce per React.
Installa Chart.js e il wrapper per React, necessari per visualizzare grafici nell’app.
Copia dei file del progetto
# Copy the custom files above the generated ones
COPY ./index.html /app/index.html
COPY ./src /app/src
# Add a command to list files
RUN ls -l /app/src/styles/ && echo "Checking files"
Copia index.html e la cartella src/ (contenente il codice React) nella directory del container. Evita di copiare tutto il progetto, prendendo solo i file necessari per la build.
Lista i file nella cartella /app/src/styles/ per verificare che siano stati copiati correttamente. Stampa un messaggio di conferma.
Build dell’app React
RUN npm run build
Compila l’applicazione con Vite, generando i file statici nella cartella /app/dist. Ottimizza il codice per la produzione.
Creazione dell’ambiente di produzione con Nginx
# Use Nginx to serve the app
FROM nginx:alpine
# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf
Usa Nginx Alpine, una versione leggera di Nginx, per servire l’applicazione React. Questo permette di distribuire l’app senza bisogno di Node.js nel container finale.
Copia un file di configurazione personalizzata di Nginx, che definisce come servire l’app React e che contiene regole per instradare richieste API al backend.
Di seguito il file nginx.conf:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri /index.html;
}
error_page 404 /index.html;
}
Il Dockerfile per il Frontend prosegue con:
# Copy the build into the Nginx container
COPY --from=build /app/dist /usr/share/nginx/html
Copia i file compilati (/app/dist) dalla fase di build nella cartella predefinita di Nginx (/usr/share/nginx/html). Questo permette a Nginx di servire direttamente i file React senza bisogno di Node.js in esecuzione.
Apertura della porta e avvio del server
# Expose port 80
EXPOSE 80
# Command to start Nginx
CMD ["nginx", "-g", "daemon off;"]
Espone la porta 80, su cui Nginx servirà l’app React. Avvia Nginx in modalità foreground (daemon off;), così il container rimane attivo.
Le API REST disponibili sul BE e loro elenco
Il sistema di videosorveglianza offre un set completo di API REST, consentendo agli utenti avanzati e agli sviluppatori di interagire con il backend in modo programmatico. Attraverso queste API, è possibile recuperare foto e video, modificare le impostazioni di configurazione e gestire il sistema senza accedere direttamente all’interfaccia grafica.
Per visualizzare l’elenco completo delle API e testarle direttamente, è disponibile una documentazione interattiva basata su Swagger/OpenAPI.
🔗 Accedi alla documentazione API:
👉 Ecco il link per visualizzare le API http://192.168.1.190:8000/docs
Di seguito, uno screenshot della documentazione API, che mostra la struttura dell’interfaccia e alcune delle operazioni disponibili:

Funzionalità principali delle API
Le API permettono di:
✅ Recuperare e scaricare foto e video acquisiti dal sistema.
✅ Configurare i parametri della videosorveglianza (es. numero massimo di foto/video, notifiche, ecc.).
✅ Monitorare lo stato del sistema e visualizzare statistiche sulle risorse utilizzate.
✅ Eseguire operazioni amministrative come riavviare il backend o aggiornare le impostazioni.
Grazie a questa interfaccia programmatica, il sistema può essere facilmente integrato con altre applicazioni o automatizzato secondo le esigenze dell’utente.
Riepilogo del Processo di Installazione del Sistema di Videosorveglianza
L’installazione del sistema di videosorveglianza su Raspberry Pi è stata progettata per essere facile e automatizzata, seguendo pochi semplici passaggi che garantiscono un setup senza complicazioni. Ecco una panoramica del processo:
📦 1. Preparazione della Raspberry Pi
- Flash dell’immagine del sistema operativo configurata o installazione manuale del sistema operativo ufficiale.
- Configurazione iniziale della Raspberry Pi, come impostazione della rete, IP fisso, timezone e abilitazione della camera.
- Collegamento via SSH per proseguire con i comandi di installazione.
🛠️ 2. Installazione dei componenti essenziali
Esegui il primo script install_docker.sh che si occupa di:
- Configurare lo swap a 2 GB, per evitare problemi durante la compilazione di pacchetti come numpy.
- Installare Docker e Docker Compose, necessari per il backend e il frontend del sistema.
- Aggiungere l’utente al gruppo Docker, evitando l’uso costante di sudo nei comandi Docker.
📌 Nota: dopo l’esecuzione di install_docker.sh, riavviare la Raspberry Pi per applicare tutte le modifiche.
⚙️ 3. Setup del Sistema di Videosorveglianza
Una volta riavviata la Raspberry Pi, esegui lo script setup_videosurveillance.sh:
- Crea i container Docker per il backend e il frontend.
- Configura i permessi dei file e delle cartelle per garantire la corretta gestione delle immagini, dei video e del database.
- Avvia i servizi del sistema (videosurveillance.service e host_service.service) e li imposta per l’avvio automatico.
🔄 4. Test del Sistema
- Verifica che il sistema sia attivo consultando i log dei servizi.
- Accedi all’interfaccia web per testare il funzionamento del sistema.
- Configura eventuali notifiche tramite Telegram, Gmail o Pushover.
🔐 5. Opzionale: Configurazione per l’accesso esterno
Se desideri accedere al sistema dall’esterno della rete locale, puoi configurare WireGuard o utilizzare una connessione VPN.
Guida all’Interfaccia web: come gestire il sistema di videosorveglianza
Introduzione
L’interfaccia web è il punto di incontro tra utente e sistema. Semplice ma potente, consente di gestire ogni aspetto del sistema di videosorveglianza in pochi click.
Dalla visualizzazione delle foto e dei video registrati, alla configurazione avanzata del sistema, passando per la gestione degli utenti e il monitoraggio delle risorse, questa interfaccia è stata progettata per garantire usabilità e accesso rapido alle informazioni più importanti.
L’interfaccia si suddivide in diverse sezioni:
- Gestione degli utenti, con controllo completo per l’amministratore.
- Visualizzazione di foto e video, organizzata in gallerie paginabili.
- Configurazione del sistema, per modificare parametri tecnici e operativi.
- Statistiche del sistema, per monitorare lo stato delle risorse e l’attività di videosorveglianza.
- Backup, con la possibilità di scaricare direttamente un file di backup dell’intero sistema.
Ora vediamo come accedere e utilizzare le varie funzionalità, partendo dalla gestione degli utenti.
Gestione utenti: amministratore e utenti normali
L’accesso al sistema di videosorveglianza è regolato tramite una pagina di login. Soltanto gli utenti registrati possono accedere alle funzionalità del sistema.
Sono previsti due tipi di account:
- Amministratore (Admin): ha accesso completo a tutte le funzionalità e può gestire il sistema in ogni suo aspetto. L’amministratore è unico e non può essere eliminato. La sua password di default è 123456 ed è fortemente consigliato cambiarla al primo accesso.
- Utenti normali: hanno accesso limitato e possono visualizzare foto, video e statistiche, oltre a modificare la propria password ma non possono cambiare la configurazione del sistema o gestire gli altri utenti.
Pagina di Login
L’interfaccia si apre con la pagina di login, dove l’utente deve inserire il proprio username e password. L’utente admin predefinito è già configurato e può essere utilizzato per il primo accesso. Successivamente, l’amministratore può aggiungere nuovi utenti o modificare le credenziali di accesso.
È fortemente raccomandato all’utente che assumerà il ruolo di amministratore di cambiare la password predefinita (123456) associata all’utente admin per motivi di sicurezza.
La foto seguente mostra la pagina di login:

La foto seguente mostra la schermata di errore in caso di inserimento di credenziali errate:

Gestione degli utenti (solo Admin)
L’amministratore ha il controllo completo sugli utenti tramite una sezione dedicata. Può:
- aggiungere nuovi utenti
- modificare gli utenti esistenti (user e/o password)
- eliminare utenti normali (ma non l’admin stesso)
L’amministratore è unico, non è prevista la possibilità di aggiungere un secondo amministratore. L’amministrazione può, attraverso una sezione dedicata della pagina, modificare il proprio userid e/o la password.
L’immagine seguente mostra la schermata di gestione utenti con elenco degli utenti e pulsanti Aggiungi, Modifica, Elimina:

Il seguente video mostra come, dal pannello di amministrazione, si possano gestire gli utenti visualizzandone l’elenco e potendo aggiungerli, modificarli o cancellarli dal sistema:
Gestione personale (per utenti normali)
Ogni utente normale può accedere alla sezione dedicata alla gestione della propria password, dove ha la possibilità di modificarla. Non è possibile, per gli utenti normali, accedere alle funzionalità avanzate di gestione o configurazione del sistema.
Per migliorare la sicurezza, è preferibile scegliere password complesse.
L’immagine seguente mostra la pagina della impostazione della password dell’ utente normale:

Il seguente video mostra come un utente può accedere alla propria pagina “Impostazioni utente” per poter modificare la propria password:
📌 Sicurezza della sessione
Per garantire la protezione dei dati, il sistema implementa un timeout automatico della sessione. Se un utente rimane inattivo per un periodo prolungato, verrà automaticamente disconnesso e reindirizzato alla pagina di login. Questo impedisce accessi non autorizzati nel caso in cui il dispositivo venga lasciato incustodito.
Gestione delle foto
La sezione Foto consente di visualizzare tutte le immagini catturate dal sistema di videosorveglianza in un’interfaccia chiara e organizzata. Ogni immagine viene mostrata come miniatura, disposta in una griglia paginata per facilitare la navigazione anche con un grande volume di dati.
Navigazione nella sezione foto
- le miniature delle immagini sono ordinate cronologicamente, con le foto più recenti in cima
- cliccando su una miniatura, si apre una vista ingrandita per osservare l’immagine in dettaglio
- una barra di paginazione permette di spostarsi facilmente tra le pagine, nel caso il numero di immagini sia superiore a quello visualizzabile su una singola schermata
- un menu a tendina consente di scegliere il numero di miniature mostrate per pagina
Funzionalità disponibili
- visualizzazione delle immagini ingrandite: ogni immagine può essere visualizzata in una dimensione maggiore per una migliore osservazione
- download delle immagini: è possibile scaricare ogni immagine cliccando sulla foto col tastro destro del mouse e cliccando sul menu la voce “download immagine”. Da smartphone/tablet: effettuare un tap prolungato sull’immagine e scegliere “Salva immagine”.
- immutabilità delle immagini: le immagini presenti nel sistema non possono essere eliminate né dagli utenti normali né dall’amministratore.
L’immagine seguente mostra la schermata della griglia di immagini con paginazione:

L’immagine seguente mostra lo screenshot della vista ingrandita di una foto:

L’immagine seguente mostra il menu di download della foto su PC:

Il seguente video mostra come visualizzare la sezione foto, navigarla tra le pagine, aprire una foto specifica e salvarla sul proprio dispositivo:
Gestione dei video
La sezione Video offre un’interfaccia per visualizzare e gestire tutti i video registrati dal sistema di videosorveglianza. Come per le foto, i video sono mostrati sotto forma di miniature, con una disposizione in griglia paginata per una navigazione semplice e intuitiva.
Navigazione nella sezione video
- le miniature dei video sono ordinate cronologicamente, con i più recenti in cima alla lista
- cliccando su una miniatura, si apre un player video integrato, che permette di visualizzare il contenuto direttamente all’interno dell’interfaccia web
- la paginazione consente di spostarsi tra le pagine per consultare tutti i video disponibili
- un menu a tendina consente di scegliere il numero di miniature mostrate per pagina
Funzionalità disponibili
- riproduzione video: ogni video può essere riprodotto direttamente dall’interfaccia senza la necessità di scaricarlo
- opzioni di visualizzazione: durante la riproduzione, è possibile utilizzare funzioni come visualizzazione a schermo intero, barra di avanzamento per un controllo completo, regolazione della velocità di riproduzione, scaricamento del video, funzione picture-in-picture
- download del video: un pulsante dedicato presente nel menu a tre puntini del player consente di scaricare i video localmente per eventuali analisi o archiviazione personale
- immutabilità dei video: anche per i video, non è prevista alcuna funzione di eliminazione tramite interfaccia web
L’immagine seguente mostra la schermata della galleria video, con le miniature e la paginazione:

L’immagine seguente mostra il player video aperto:

L’immagine seguente mostra il player video con il menu delle opzioni possibili aperto:

Il seguente video mostra come visualizzare la sezione video, navigarla tra le pagine, aprire un video specifico ed eseguire delle operazioni su di esso:
Configurazione del sistema
La sezione Configuration Management è accessibile solo all’amministratore e consente di personalizzare le impostazioni operative del sistema di videosorveglianza. Questa sezione è divisa in tre aree principali:
- Configurazione Generale (parametri principali del sistema)
- Gestione delle Notifiche (Telegram, Pushover, Email)
- Configurazione di Motion (parametri per il rilevamento dei movimenti)
Dopo aver apportato modifiche ai parametri, è possibile salvarle cliccando su Save Configuration e riavviare il servizio di videosorveglianza cliccando su Restart Service per renderle effettive.
Configurazione Generale
Questa sezione permette di gestire le impostazioni principali del sistema:
- Initial Delay (seconds): tempo di attesa prima che inizi il monitoraggio (default: 15 secondi) per dare il tempo a Motion di assestarsi all’avvio.
- Process Interval (seconds): intervallo tra l’elaborazione delle foto (default: 10 secondi).
- Max Photos: numero massimo di foto da archiviare prima della cancellazione automatica (default: 500).
- Max Videos: numero massimo di video archiviabili prima della cancellazione automatica (default: 50).
- Batch Size: numero di foto elaborate in un singolo batch di caricamento (default: 50).
Questi parametri permettono di gestire il carico di lavoro del sistema e la conservazione delle immagini in base alla capacità di memoria disponibile.
Gestione delle Notifiche
Il sistema supporta tre modalità di notifica per avvisare l’utente di nuovi eventi rilevati:
- Pushover: notifiche push dirette su smartphone tramite l’API Pushover. È necessario inserire il proprio Pushover Token e la User Key.
- Telegram: invio di notifiche e immagini tramite bot Telegram. È necessario attivare l’opzione e inserire il Telegram Token e l’ID Utente.
- Gmail: invio delle notifiche via email tramite il server SMTP di Google. Occorre specificare:
- SMTP Server (default: smtp.gmail.com)
- SMTP Port (default: 587)
- Email mittente e destinatario
- App Password per l’autenticazione.
Le notifiche possono essere abilitate o disabilitate tramite un apposito checkbox per ogni servizio. Il sistema supporta anche la visualizzazione delle password, che possono essere rese visibili o nascoste per motivi di sicurezza.
Configurazione di Motion (motion.conf)
Questa sezione gestisce i parametri del software Motion, responsabile del rilevamento dei movimenti e della cattura di foto/video.
- Output Mode: seleziona se salvare solo foto o solo video.
- Threshold: soglia di sensibilità per rilevare un movimento (default: 500).
- Minimum Motion Frames: numero minimo di frame con movimento per generare un evento (default: 2).
- Event Gap: tempo minimo tra due eventi consecutivi (default: 15 secondi).
- Noise Level: livello di rumore accettabile prima di considerare un evento come valido (default: 64).
- Noise Tune: attivazione del filtro automatico per regolare il livello di rumore.
- Despeckle Filter: selezione del filtro per la rimozione del rumore nei frame catturati (opzione
EedDl
predefinita). - Auto Brightness: regolazione automatica della luminosità per migliorare la qualità delle immagini.
Questa sezione consente di ottimizzare il comportamento del sistema in base alle condizioni ambientali, evitando falsi positivi e migliorando l’affidabilità delle notifiche.
Dopo ogni modifica, è necessario salvare le impostazioni con Save Configuration e riavviare il sistema con Restart Service per applicare le modifiche.

Statistiche e monitoraggio del sistema
La sezione Statistiche offre una panoramica dettagliata sullo stato del sistema di videosorveglianza, consentendo di monitorare sia la quantità di media archiviati (foto e video) sia l’utilizzo delle risorse hardware. I dati vengono aggiornati automaticamente ogni 10 secondi, garantendo informazioni sempre attuali.
Statistiche sui media
La prima parte della sezione si concentra sulle statistiche relative a foto e video presenti nel sistema. Ecco cosa troverai:
- Foto totali: il numero totale di foto salvate
- Foto ultimo mese: il numero di foto scattate nell’ultimo mese
- Dimensione totale foto: lo spazio occupato da tutte le foto sul sistema
- Video totali: il numero totale di video registrati
- Video ultimo mese: il numero di video registrati nell’ultimo mese
- Dimensione totale video: lo spazio occupato da tutti i video
- Durata totale video: la durata complessiva di tutti i video in minuti
Statistiche di sistema
La seconda parte mostra le risorse di sistema e il loro utilizzo in tempo reale:
- Utilizzo CPU (%): percentuale di utilizzo del processore
- Utilizzo Memoria (%): percentuale di memoria RAM utilizzata
- Utilizzo Disco (%): percentuale di spazio su disco utilizzato
- Utilizzo Swap (%): percentuale di swap utilizzato
Grafici e visualizzazioni
Per rendere più comprensibili i dati, la sezione offre due tipi di grafici:
- Grafico a barre: mostra il numero totale di foto e video.
- Grafico a ciambella (Doughnut Chart): visualizza l’utilizzo delle risorse del sistema, come CPU, memoria, disco e swap, in percentuale.
Questi grafici aiutano a comprendere immediatamente lo stato generale del sistema e a individuare eventuali problemi di performance.
L’immagine seguente mostra la sezione Statistiche con i dati visibili e i grafici ben in evidenza per mostrare come viene visualizzato l’utilizzo delle risorse:

Backup automatico e download
Un sistema di videosorveglianza efficace deve garantire la sicurezza non solo delle riprese ma anche dell’integrità dei dati. Per questo motivo, è stato implementato un meccanismo di backup automatico, che salva periodicamente i dati più importanti in un file denominato backup.zip.
Il backup include:
- le foto archiviate: per garantire che nessun evento venga perso.
- i video registrati: per mantenere uno storico completo.
- il database SQLite contenente la configurazione del sistema e gli utenti: contenente la configurazione del sistema e le credenziali degli utenti.
Backup automatico
Il backup viene creato automaticamente a intervalli regolari, configurabili nel file main.py. Questo significa che il processo avviene in modo completamente automatico, senza bisogno di intervento manuale. Questo garantisce che, in caso di guasto o errore, i dati possano essere facilmente recuperati utilizzando lo script restore_backup.sh.
Pulsante di download del backup
Per comodità dell’amministratore, nella pagina principale dell’interfaccia web è disponibile un pulsante di download. Questo pulsante consente di scaricare il file backup.zip più recente direttamente dal browser, senza dover accedere fisicamente alla Raspberry Pi.
📌 Nota Bene: il download è disponibile solo per l’amministratore del sistema. Gli utenti standard non possono accedere a questa funzionalità per garantire la sicurezza dei dati.
Logout: disconnessione sicura dal sistema
Il pulsante di Logout, visibile a tutti gli utenti, consente di uscire in modo sicuro dall’applicazione web. Questa funzione garantisce la protezione dell’account e impedisce l’accesso non autorizzato da parte di terzi.
Cosa fa il logout?
- cancella il token di sessione utilizzato per l’autenticazione, invalidando l’accesso corrente
- reindirizza l’utente alla pagina di login, dove sarà necessario inserire nuovamente le credenziali per accedere
- protegge i dati sensibili, evitando che altri possano accedere alle informazioni se il dispositivo viene lasciato incustodito
💡 Consiglio per la sicurezza: è buona norma effettuare sempre il logout al termine di ogni sessione, soprattutto se si utilizza il sistema su un dispositivo condiviso o pubblico.
📡 Accesso remoto sicuro con WireGuard
L’applicazione di videosorveglianza funziona perfettamente sulla rete locale, ma cosa succede se vogliamo accedere alle immagini e ai video da remoto? La soluzione più sicura è WireGuard, una VPN moderna, veloce e facile da configurare.
Perché usare WireGuard per la videosorveglianza?
WireGuard è una VPN moderna, veloce e sicura, progettata per essere più semplice ed efficiente rispetto a soluzioni più datate come OpenVPN o IPSec. La sua leggerezza e facilità d’uso lo rendono particolarmente adatto per dispositivi a bassa potenza come il Raspberry Pi, che è il cuore del nostro sistema di videosorveglianza.
Ma perché abbiamo bisogno di una VPN?
- Accesso remoto sicuro 🔒 → WireGuard permette di collegarsi alla propria rete domestica da qualsiasi parte del mondo, garantendo la sicurezza delle comunicazioni grazie alla crittografia avanzata.
- Evita configurazioni complicate di NAT e IP dinamico 🚀 → se il tuo provider Internet cambia il tuo IP pubblico frequentemente (come accade con le connessioni domestiche), WireGuard ti consente di mantenere sempre un collegamento stabile.
- Banda ridotta e latenza minima ⚡ → WireGuard è più efficiente rispetto ad altre VPN, riducendo il consumo di CPU e ottimizzando il traffico dati, un aspetto fondamentale quando si accede a streaming video in tempo reale.
- Facilità di configurazione 🛠️ → a differenza di altre soluzioni, WireGuard richiede solo poche righe di configurazione per funzionare.
Utilizzando WireGuard, possiamo accedere in modo sicuro alla nostra interfaccia di videosorveglianza senza dover esporre i servizi della Raspberry Pi direttamente su Internet, evitando così potenziali attacchi e vulnerabilità.
Problemi comuni e verifiche preliminari (da leggere prima di procedere!)
Prima di procedere con l’installazione e la configurazione di WireGuard, è fondamentale verificare alcuni aspetti chiave. Se questi requisiti non sono soddisfatti, WireGuard potrebbe non funzionare correttamente o richiedere configurazioni aggiuntive.
✅ 1. Verificare se il router ha un IP pubblico
Per poter accedere alla Raspberry Pi da remoto, il router deve avere un IP pubblico assegnato dal provider. Alcuni provider utilizzano CG-NAT (Carrier-Grade NAT), che impedisce il collegamento diretto dall’esterno.
🔹 Come verificare? Apri il terminale sulla Raspberry Pi e digita:
curl -s -4 https://ifconfig.me
Questo comando ti mostrerà il tuo IP pubblico.
Ora accedi all’interfaccia web del tuo router e cerca la sezione che mostra l’IP WAN (di solito si trova nelle impostazioni di rete o stato della connessione).
Se l’IP pubblico ottenuto con curl è uguale a quello mostrato dal router, significa che hai un IP pubblico reale e puoi procedere.
Se gli indirizzi sono diversi, è probabile che il tuo provider usi CG-NAT, il che rende impossibile la connessione diretta. In questo caso, devi contattare il provider e richiedere un IP pubblico statico o dinamico.
Alternativa: Se non riesci a trovare l’IP pubblico dal router, puoi controllarlo anche da un qualsiasi dispositivo connesso alla rete visitando il sito:
👉 https://whatismyipaddress.com
✅ 2. Aprire la porta 51820 UDP nel router
WireGuard utilizza di default la porta 51820/UDP per le connessioni VPN. Se questa porta è bloccata dal firewall del router, il client non sarà in grado di connettersi.
🔹 Come aprire la porta sul router?
- Accedi all’interfaccia del router (di solito si trova all’indirizzo 192.168.1.1 o 192.168.0.1).
- Cerca la sezione Port Forwarding o Virtual Server.
- Aggiungi una nuova regola:
- Protocollo: UDP
- Porta esterna: 51820
- IP interno: l’indirizzo IP della Raspberry Pi (es. 192.168.1.190)
- Porta interna: 51820
💡 Nota: se hai un firewall attivo sulla Raspberry Pi, assicurati che la porta 51820/UDP sia aperta anche lì con:
sudo ufw allow 51820/udp
Eventuali problemi con la porta per WireGuard e configurazione del router
Per poter accedere da remoto alla tua Raspberry Pi attraverso WireGuard, devi aprire una porta nel router e reindirizzarla alla Raspberry.
Di default, WireGuard utilizza la porta UDP 51820, ma non tutti i router la accettano.
Problemi con la porta 51820?
Se il tuo router rifiuta questa porta, come accade con alcuni provider che impongono limiti sulle porte disponibili, dovrai scegliere una porta compresa tra 32768 e 40959, che è l’intervallo delle porte effimere ufficiali consigliate per UDP.
Come scegliere la porta giusta?
Controlla il tuo router: se provi a configurare il port forwarding e ricevi un errore che indica che la porta è fuori intervallo, scegli un’altra porta all’interno di 32768-40959.
Evita porte già usate da altri servizi: non scegliere porte riservate a VPN commerciali, server di gioco o VoIP.
Mantieni UDP: WireGuard funziona solo con il protocollo UDP, quindi non provare con TCP.
Quindi imposta lo script install_wireguard.sh coerentemente con la porta scelta (e aperta sul router). Segui poi i paragrafi successivi per l’installazione di WireGuard.
✅ 3. Assicurarsi che la Raspberry Pi abbia una connessione stabile
WireGuard crea un tunnel VPN sicuro tra i dispositivi, quindi è essenziale che la Raspberry Pi sia sempre online e raggiungibile.
🔹 Verifiche da fare:
- La Raspberry Pi è connessa via cavo Ethernet o ha un Wi-Fi stabile?
- Ha un IP statico all’interno della rete locale? Per evitare problemi, è consigliato assegnare un IP statico alla Raspberry Pi dal router (noi abbiamo già provveduto ad assegnarle l’IP statico 192.168.1.190.
- Il client (telefono o PC) ha una connessione dati attiva e funzionale?
💡 Se uno di questi passaggi fallisce, WireGuard potrebbe non funzionare correttamente!
📌 Risolvi prima questi problemi prima di passare all’installazione.
Installazione di WireGuard su Raspberry Pi
Per semplificare l’installazione e la configurazione, utilizzeremo un script automatico che installerà WireGuard, genererà le chiavi e configurerà il server VPN.
📌 Installazione automatizzata con lo script install_wireguard.sh
Ho creato uno script che automatizza l’intero processo di installazione e configurazione di WireGuard sulla Raspberry Pi. Tale script si trova nella cartella di progetto videosurveillance.
Rendi eseguibile lo script:
chmod +x install_wireguard.sh
Esegui lo script:
sudo ./install_wireguard.sh
Al rermine dell’esecuzione ti dovrebbe apparire sulla shell un QR code che ti servirà fra poco per abbinare l’applicazione WireGuard sul tuo cellulare alla VPN sulla Raspberry. Lo script install_wireguard.sh installerà WireGuard, configurerà il server e genererà automaticamente la configurazione per il client. Se vuoi vedere il file di configurazione del server, puoi trovarlo in /etc/wireguard/wg0.conf.
Allo stesso modo, il file di configurazione per il client sarà disponibile in client.conf e potrà essere importato direttamente nell’app WireGuard per smartphone tramite QR code. Questa app, disponibile per Android e iOS, dovrà essere installata prima di procedere alla configurazione del client, come verrà spiegato nel paragrafo successivo. Alla fine dell’esecuzione dello script, verrà generato e mostrato un QR code, che potrà essere scansionato direttamente dall’app per configurare automaticamente la connessione.
Configurazione del client su smartphone
Installazione di WireGuard su Android/iOS
Per connettere lo smartphone alla Raspberry Pi tramite VPN, è necessario installare l’app WireGuard, disponibile sui principali store:
- Android: Google Play Store
- iOS: Apple App Store
Dopo aver installato l’app, aprila e segui i passaggi successivi.
Importazione della configurazione
Ci sono due modi per importare la configurazione del client nel telefono:
📌 Metodo 1: Scansione del QR Code (consigliato)
Dopo aver eseguito lo script install_wireguard.sh, verrà generato automaticamente un QR code con la configurazione del client. Per importarlo:
- Apri l’app WireGuard.
- Tocca il pulsante “+” in basso a destra.
- Seleziona “Scansiona un codice QR dalla fotocamera”.
- Punta la fotocamera dello smartphone verso il QR code mostrato nel terminale della Raspberry Pi.
- Dopo la scansione, assegna un nome alla connessione (es. RaspberryPi VPN) e salva.
- Modifica manualmente la configurazione:
- Apri la connessione appena creata.
- Nella sezione Peer, trova la voce Allowed IPs.
- Sostituisci 0.0.0.0/0 con 192.168.1.190/32.
- Salva le modifiche.
📌 Metodo 2: Configurazione manuale
Se non puoi scansionare il QR code, puoi configurare WireGuard manualmente:
- Apri l’app WireGuard e tocca “+” in basso a destra.
- Seleziona “Crea da zero”.
- Nella schermata di configurazione:
- Nome della connessione → “RaspberryPi VPN”
- Chiave privata → Tocca “Genera” per creare una nuova chiave
- Indirizzo IP → Inserisci 10.0.0.2/32
- Server DNS → Inserisci 8.8.8.8 (Google DNS)
- Tocca “Aggiungi peer” e compila così:
- Chiave pubblica → Inserisci la chiave pubblica del server (la trovi in client.conf)
- Endpoint → Inserisci IP_PUBBLICO:51820 (sostituisci con l’IP pubblico del router)
- Allowed IPs → Inserisci 192.168.1.190/32
- Keepalive persistente → 25
- Tocca “Salva” per completare la configurazione.
A questo punto devi riavviare la Raspberry col comando:
sudo reboot
Terminato il riavvio puoi connetterti alla VPN!
Connessione alla Raspberry Pi e test di funzionamento
- Apri l’app WireGuard.
- Tocca la connessione “RaspberryPi VPN”.
- Attiva il tunnel toccando l’interruttore ON/OFF.
Se la connessione è attiva, puoi testarla con un ping dalla Raspberry Pi al telefono:
ping 10.0.0.2
oppure con un ping dal telefono alla Raspberry Pi:
ping 192.168.1.190
Se tutto è andato a buon fine, puoi accedere all’interfaccia web della videosorveglianza dal telefono:
http://192.168.1.190:3000
NOTA: se hai necessità di rivedere il QR code generato a partire dal file di configurazione client.conf nella cartella di progetto videosurveillance della Raspberry dai il comando:
qrencode -t UTF8 < client.conf
Configurazione del client su PC (fortemente raccomandata)
L’uso della videosorveglianza su PC è la soluzione ideale per sfruttare al massimo l’interfaccia e tutte le funzionalità disponibili. Lo schermo del pc è decisamente più adatto a riprodurre tutti gli elementi di una interfaccia complessa come la nostra. Qui vediamo come configurare WireGuard su Windows, Linux e macOS.
Installazione di WireGuard su Windows/Linux/macOS
Per prima cosa, è necessario installare il client WireGuard sul computer:
- Windows: scarica l’installer dal sito ufficiale di WireGuard https://www.wireguard.com/install/ e installalo come una normale applicazione.
- Linux: apri un terminale e installa WireGuard con: sudo apt update && sudo apt install wireguard (su Ubuntu/Debian) o il comando equivalente per la tua distribuzione.
- macOS: scarica l’applicazione WireGuard dal Mac App Store o usa il comando: brew install wireguard-tools se hai Homebrew installato.
Generazione e importazione della configurazione
Dopo aver installato WireGuard, dobbiamo importare la configurazione del client.
Se hai già eseguito lo script install_wireguard.sh sulla Raspberry Pi, dovresti avere il file client.conf generato automaticamente. Ora hai due modi per importarlo su PC:
- Metodo 1: Copia manuale del file
- Trasferisci il file client.conf dal Raspberry Pi al PC (tramite USB, SCP, email, ecc.).
- Apri l’applicazione WireGuard e clicca su Importa tunnel da file.
- Seleziona client.conf e conferma.
- Attiva la connessione con il pulsante “Attiva”.
- Metodo 2: Creazione manuale del tunnel Se non puoi trasferire il file, puoi copiare manualmente il suo contenuto:
- Apri l’app WireGuard.
- Clicca su Aggiungi Tunnel > Crea da zero.
- Inserisci il nome del tunnel (es. “Videosorveglianza”).
- Copia e incolla il contenuto di client.conf nei campi corrispondenti.
- Salva e attiva la connessione.
Accesso alla videosorveglianza dal browser
Una volta che la connessione VPN è attiva, puoi accedere all’interfaccia della videosorveglianza direttamente dal browser.
- Apri il browser sul tuo PC (Chrome, Firefox, Edge, ecc.).
- Digita l’indirizzo 192.168.1.190:3000 nella barra degli indirizzi.
- Inserisci le credenziali di accesso.
- Ora puoi navigare nell’interfaccia della videosorveglianza, controllare le immagini e i video registrati con la massima comodità!
Vantaggi dell’uso su PC
- Schermo più grande per visualizzare meglio le registrazioni.
- Navigazione più comoda con mouse e tastiera.
- Velocità maggiore rispetto all’uso su smartphone.
Per questi motivi, consiglio di usare il PC per la gestione quotidiana del sistema di videosorveglianza. Lo smartphone può essere utile per controlli rapidi o notifiche, ma l’esperienza completa si ottiene su desktop!
Conclusione: un accesso remoto sicuro e senza compromessi
L’integrazione di WireGuard nel sistema di videosorveglianza su Raspberry Pi rappresenta una soluzione potente, sicura ed efficiente per accedere ai propri dispositivi da qualsiasi parte del mondo.
Grazie alla criptazione moderna e all’estrema leggerezza del protocollo, WireGuard garantisce un tunnel VPN sicuro senza impattare le prestazioni della rete o della Raspberry Pi.
📌 Accesso ovunque e in totale sicurezza
Con questa configurazione, è possibile monitorare la videosorveglianza da remoto senza dover esporre la Raspberry Pi direttamente su Internet. Questo elimina i rischi legati all’apertura di porte e a eventuali attacchi.
📌 Configurazione rapida e senza compromessi
L’installazione e la configurazione sono automatizzate dallo script fornito, rendendo il processo semplice e veloce anche per chi non ha esperienza con le VPN. In pochi minuti, l’utente può collegarsi alla propria videosorveglianza da smartphone o PC.
📌 Sicurezza della rete domestica preservata
A differenza di altre soluzioni che richiedono il port forwarding e l’esposizione diretta del dispositivo su Internet, WireGuard permette un accesso remoto senza compromettere la sicurezza della rete locale, proteggendo la Raspberry Pi e gli altri dispositivi connessi.
Conclusioni e possibili sviluppi futuri
Abbiamo realizzato un sistema di videosorveglianza completo, basato su Raspberry Pi, Docker e WireGuard, in grado di offrire una soluzione scalabile, sicura e accessibile da remoto. Grazie a una combinazione di tecnologie moderne, il sistema permette di registrare video e foto, visualizzarli tramite un’interfaccia intuitiva e proteggere l’accesso con una VPN leggera ma potente.
Ma il progetto può essere ulteriormente migliorato con le numerose possibilità di espansione e personalizzazione che puoi considerare per adattarlo alle tue esigenze:
🔧 Migliorie possibili
- Notifiche avanzate → implementare notifiche più dettagliate via Telegram, Gmail o Pushover o aggiungendo altri sistemi.
- Integrazione con AI → utilizzare modelli di riconoscimento facciale o motion detection basati su AI per migliorare la rilevazione degli eventi.
- Supporto a più telecamere → estendere il supporto a più telecamere per monitorare diverse zone contemporaneamente.
- Archiviazione su cloud o NAS → implementare un sistema di backup automatico su un servizio cloud o un NAS locale per avere uno storico sempre accessibile.
- Interfaccia utente migliorata → ottimizzare la UI per una migliore esperienza su dispositivi mobili o aggiungere funzionalità come filtri di ricerca avanzati.
Con queste possibili evoluzioni, il sistema può trasformarsi in una piattaforma ancora più potente e versatile.
🔍 Se vuoi approfondire o migliorare ulteriormente il progetto, sentiti libero di sperimentare e condividere le tue modifiche! 🚀
🔗 Altri progetti interessanti
Se ti è piaciuto questo progetto, potresti trovare utili anche questi articoli:
- Videosorveglianza con ESP32 e Telegram – un sistema compatto che cattura immagini e le invia direttamente su Telegram.
- Telecamera motorizzata WiFi: monitoraggio e controllo da remoto via web – un’ESP32 con una telecamera controllabile a distanza tramite un’interfaccia web.
Newsletter
Se vuoi essere aggiornato sui nuovi articoli, iscriviti alla newsletter. Prima dell’iscrizione alla newsletter leggi la pagina Privacy Policy (UE)
Se ti vuoi disiscrivere dalla newsletter clicca sul link che troverai nella mail della newsletter.
🔗 Seguici anche sui nostri canali social per non perdere nessun aggiornamento!
📢 Unisciti al nostro canale Telegram per ricevere aggiornamenti in tempo reale.
🐦 Seguici su Twitter per rimanere sempre informato sulle nostre novità.
Grazie per far parte della nostra community TechRM! 🚀