Compare commits

...

58 Commits

Author SHA1 Message Date
865fde00c2 ajout de license APACHE 2.0 2026-04-03 21:20:31 +02:00
f275ad60f0 félicitations 2026-04-03 18:35:33 +02:00
9770617cd4 finally the end of the road 2026-04-02 23:09:57 +02:00
a14ebc3243 finally the end of the road 2026-04-02 22:20:07 +02:00
a30b9a9ba2 finally the end of the road 2026-04-02 22:19:52 +02:00
01bf68a77f maj REadMe 2026-04-02 15:18:22 +02:00
fe093d6c9d maj Readme 2026-04-02 15:15:44 +02:00
23bc302168 maj Readme 2026-04-02 15:05:25 +02:00
1a93d18369 maj Readme 2026-04-02 15:04:31 +02:00
a8f236427e maj 2026-04-02 15:00:09 +02:00
snow
825f153f51 Merge branch 'main' of https://github.com/maxdrk/loustique-home 2026-04-02 11:29:35 +02:00
snow
9977ce7797 WOLA CHANGEMENT DES BUTTONS 2026-04-02 11:29:29 +02:00
GigiTheGiraffe
cd9d837135 piou 2026-04-02 10:37:13 +02:00
902a626776 tentative de foutre des points 2026-04-01 22:33:28 +02:00
fea6467396 final 2026-04-01 22:26:25 +02:00
23f7f78178 final 2026-04-01 22:23:25 +02:00
0ea3e846f1 servo 2026-04-01 18:41:50 +02:00
41312b0f75 maj de 18h38 2026-04-01 18:39:00 +02:00
0ab04110ad maj alarme 2026-04-01 16:17:21 +02:00
cc0365a2e1 maj alarme 2026-04-01 15:54:46 +02:00
904d5e3475 ajout du ldr 2026-04-01 14:27:53 +02:00
9c33d832b7 maj interface web 2026-04-01 01:03:44 +02:00
e7a55b4788 ajout main.py 2026-04-01 01:03:44 +02:00
maxdrk
b7ef783790 Delete venv directory 2026-03-31 23:59:08 +02:00
0dfc9356fe réorganisation du tree 2026-03-31 23:58:23 +02:00
4f2d3018b5 maj fastapi 2026-03-31 23:50:23 +02:00
059552eb81 1.3 2026-03-31 23:37:54 +02:00
fa94a4564d on voit le bout du tunnel 2026-03-31 20:53:02 +02:00
6d8f7dad4a Maj 7segment 2026-03-31 20:36:38 +02:00
7790694fb9 Maj bouton 2026-03-31 20:31:02 +02:00
af8e674cb9 maj des API 2026-03-31 20:09:52 +02:00
bf5b24214c maj vers BCM 2026-03-31 20:09:26 +02:00
c378be1695 . 2026-03-30 23:22:11 +02:00
98aa5fceb1 . 2026-03-30 23:21:57 +02:00
488289e2cf . 2026-03-30 23:21:28 +02:00
76e8ecadc1 . 2026-03-30 23:19:01 +02:00
bb566f7da2 . 2026-03-30 23:17:08 +02:00
703f7df12f piou piou 2026-03-30 22:10:28 +02:00
dbf6e807ae mise à jour numéro je sais pas combien 2026-03-30 22:09:23 +02:00
fd17c22dfc maj README 2026-03-30 21:47:46 +02:00
5d6a2bb32b api presque fonctionnel 2026-03-30 21:46:43 +02:00
72a86ff512 wallah pardon sur le coran de l eglise c'est pas de ma faute 2026-03-30 20:53:53 +02:00
8c3713eff6 maj par maim 2026-03-30 20:04:58 +02:00
785508ba53 maj README 2026-03-30 20:02:03 +02:00
0d3f9d82d4 maj README 2026-03-30 20:01:32 +02:00
951f073481 maj README 2026-03-30 19:59:45 +02:00
9a109c49e6 1.1 2026-03-30 19:55:04 +02:00
4e7a41d7aa maj readme 2026-03-30 18:22:47 +02:00
119027dc00 ajout threading 2026-03-30 18:20:42 +02:00
87774e5303 liaison rfid html 2026-03-30 18:18:50 +02:00
0db365014a liaison rfid html 2026-03-30 18:16:20 +02:00
1eee629102 liaison rfid 2026-03-30 18:09:36 +02:00
474a10aa43 1.0 2026-03-30 17:49:18 +02:00
50a48c5292 liaison python et html 2026-03-30 14:00:03 +02:00
afee1262eb mise en place code panda sur html 2026-03-27 15:00:11 +01:00
9d3c201672 wola j'avais oublié ces codes 2026-03-27 13:33:32 +01:00
f5ba39da8d code esteban 2026-03-27 13:29:22 +01:00
1f84b5bc09 mise à jour par Greg 2026-03-26 14:47:53 +01:00
2033 changed files with 2617 additions and 362035 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
*.pem
.env
venv

15
LICENSE Normal file
View File

@@ -0,0 +1,15 @@
Copyright 2026 Les Loustiques
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Source: http://opensource.org/licenses/Apache-2.0

View File

@@ -18,10 +18,13 @@ L'application repose sur un serveur **Flask** (Python) qui expose une API et ser
loustiques-home/ loustiques-home/
├── flask/ ├── flask/
│ ├── main.py # Point d'entrée Flask — routes et API │ ├── main.py # Point d'entrée Flask — routes et API
├── .env # Variable d'environnement
│ ├── auth.py # Authentification et gestion des utilisateurs │ ├── auth.py # Authentification et gestion des utilisateurs
│ ├── add_user.py # Ajout d'utilisateur (appelé par l'admin) │ ├── add_user.py # Ajout d'utilisateur (appelé par l'admin)
│ ├── led.py # Contrôle de la LED via GPIO │ ├── led.py # Contrôle de la LED via GPIO
│ ├── log.py # Configuration du système de logs │ ├── log.py
├── run_flask.sh # Script de démarrage du serveur
├── requirement.txt # Dépendances Python # Configuration du système de logs
│ └── templates/ │ └── templates/
│ ├── index.html # Page de connexion │ ├── index.html # Page de connexion
│ ├── dashboard.html # Tableau de bord principal │ ├── dashboard.html # Tableau de bord principal
@@ -33,9 +36,7 @@ loustiques-home/
│ └── ssl/ │ └── ssl/
│ ├── cert.pem # Certificat SSL (généré automatiquement) │ ├── cert.pem # Certificat SSL (généré automatiquement)
│ └── key.pem # Clé privée SSL (générée automatiquement) │ └── key.pem # Clé privée SSL (générée automatiquement)
├── run_flask.sh # Script de démarrage du serveur
├── requirement.txt # Dépendances Python
└── .env # Variables d'environnement (non versionné)
``` ```
--- ---
@@ -58,6 +59,10 @@ sudo bash run_flask.sh
Génère un certificat SSL auto-signé (`cert.pem` + `key.pem`) dans `web_secu/ssl/`. Le certificat est valable 365 jours et est émis pour `loustiques.local`. Si le certificat existe déjà, le script ne le régénère pas. Génère un certificat SSL auto-signé (`cert.pem` + `key.pem`) dans `web_secu/ssl/`. Le certificat est valable 365 jours et est émis pour `loustiques.local`. Si le certificat existe déjà, le script ne le régénère pas.
## Note
le script run_flash.sh s'occupe de la génération pour que le serveuf flask ai d'office un certificat valide
``` ```
C=BE / ST=Brabant Wallon / L=Louvain-La-Neuve / O=Les Loustiques / OU=EPHEC / CN=loustiques.local C=BE / ST=Brabant Wallon / L=Louvain-La-Neuve / O=Les Loustiques / OU=EPHEC / CN=loustiques.local
``` ```
@@ -98,6 +103,7 @@ Gère l'authentification des utilisateurs et la récupération des comptes.
- `init()` — ouvre une connexion MySQL à partir des variables d'environnement `.env` - `init()` — ouvre une connexion MySQL à partir des variables d'environnement `.env`
- `login(username, password)` — vérifie les identifiants en comparant le mot de passe avec le hash bcrypt stocké en base - `login(username, password)` — vérifie les identifiants en comparant le mot de passe avec le hash bcrypt stocké en base
- `get_users()` — retourne la liste de tous les utilisateurs (username, rôle, date de création) - `get_users()` — retourne la liste de tous les utilisateurs (username, rôle, date de création)
- `get_user_by_rfid` - s'occupe de récupérer et chercher l'utilisateur lié à son RFID dans la base de données
--- ---
@@ -107,11 +113,6 @@ Permet d'ajouter un nouvel utilisateur en base de données. Cette fonction est a
--- ---
### `led.py`
Contient la fonction `led(utilisateur)` qui simule (ou déclenche réellement sur le Pi) l'allumage d'une LED. Le code GPIO est commenté pour permettre les tests hors Raspberry Pi. Chaque déclenchement est logué avec le nom de l'utilisateur qui en est à l'origine.
---
### `main.py` ### `main.py`
@@ -240,9 +241,16 @@ CREATE TABLE Auth (
| `bcrypt` | Hashage des mots de passe | | `bcrypt` | Hashage des mots de passe |
| `python-dotenv` | Chargement du fichier `.env` | | `python-dotenv` | Chargement du fichier `.env` |
| `gpiozero` | Contrôle des GPIOs du Raspberry Pi | | `gpiozero` | Contrôle des GPIOs du Raspberry Pi |
| `mfrc` | Bibliothèque destiné au RFID
--- ---
### Note
On a essayé de le finaliser au mieux, mais lautomatisation reste incomplète et larborescence des deux Raspberry Pi nest pas identique. Des améliorations doivent encore être apportées aux scripts afin dassurer une automatisation complète. Il est également possible que certaines informations du README soient manquantes ou ne soient plus en adéquation avec larborescence, celle-ci ayant subi de nombreuses mises à jour techniques et logiques.
## Groupe ## Groupe
Projet réalisé par **Les Loustiques** — étudiants à l'**EPHEC** (Louvain-La-Neuve). Projet réalisé par **Les Loustiques** — étudiants à l'**EPHEC** (Louvain-La-Neuve).
©️ **Les loustiques**

View File

@@ -1,17 +0,0 @@
from machine import Pin,ADC
ldr_sensor_pin = 35
adc = ADC(Pin(ldr_sensor_pin))
adc.width(ADC.WIDTH_10BIT)
adc.atten(ADC.ATTN_11DB)
def luminosite_detection():
while True:
luminosite = adc.read()
print (luminosite)
if luminosite > 300:
led_verte_luminosite.on()
else:
led_verte_luminosite.off()
utime.sleep(0.5)

View File

@@ -1,13 +0,0 @@
from machine import Pin,PWM
buzzer_pin = 11
buzzer = PWM(Pin(buzzer_pin), freq=440, duty=0)
def activate_alarm():
for _ in range(3):
buzzer.duty(512)
utime.sleep(0.5)
buzzer.duty(0)
utime.sleep(0.5)

View File

@@ -0,0 +1,10 @@
import Adafruit_DHT as dht
capteur = dht.DHT11
pin = 25
def lire_temperature():
humidite, temperature = dht.read_retry(capteur, pin)
if temperature is not None:
return temperature
else:
return 0

12
composants/byPanda/LDR.py Normal file
View File

@@ -0,0 +1,12 @@
import RPi.GPIO as GPIO
LDR_PIN = 20
GPIO.setmode(GPIO.BCM)
GPIO.setup(LDR_PIN, GPIO.IN)
def lire_etat():
if GPIO.input(LDR_PIN) == GPIO.HIGH:
return "Nuit"
return "Jour"

Binary file not shown.

View File

@@ -0,0 +1,186 @@
import RPi.GPIO as GPIO
import time
import threading
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
PIN_LED_R = 17
PIN_LED_G = 22
PIN_LED_B = 3
PIN_PIR = 23
PIN_BUZZER = 18
ROWS = [5, 6, 13, 19]
COLS = [26, 12, 16, 20]
KEYPAD_MAP = [
['1', '2', '3', 'A'],
['4', '5', '6', 'B'],
['7', '8', '9', 'C'],
['*', '0', '#', 'D'],
]
CODE_SECRET = "1234"
GPIO.setup(PIN_LED_R, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PIN_LED_G, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PIN_LED_B, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PIN_BUZZER, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PIN_PIR, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
for row in ROWS:
GPIO.setup(row, GPIO.OUT, initial=GPIO.HIGH)
for col in COLS:
GPIO.setup(col, GPIO.IN, pull_up_down=GPIO.PUD_UP)
etat = "Désarmée"
etat_lock = threading.Lock()
_stop_buzzer = threading.Event()
_thread_buzzer = None
def led(r=False, g=False, b=False):
GPIO.output(PIN_LED_R, GPIO.HIGH if r else GPIO.LOW)
GPIO.output(PIN_LED_G, GPIO.HIGH if g else GPIO.LOW)
GPIO.output(PIN_LED_B, GPIO.HIGH if b else GPIO.LOW)
def bip(nb=1, duree=0.08, pause=0.12):
for _ in range(nb):
GPIO.output(PIN_BUZZER, GPIO.HIGH)
time.sleep(duree)
GPIO.output(PIN_BUZZER, GPIO.LOW)
time.sleep(pause)
def _buzzer_continu(stop_event):
while not stop_event.is_set():
GPIO.output(PIN_BUZZER, GPIO.HIGH)
time.sleep(0.5)
GPIO.output(PIN_BUZZER, GPIO.LOW)
time.sleep(0.5)
GPIO.output(PIN_BUZZER, GPIO.LOW)
def etat_alarme():
with etat_lock:
return etat
def lire_touche():
for i, row in enumerate(ROWS):
GPIO.output(row, GPIO.LOW)
for j, col in enumerate(COLS):
if GPIO.input(col) == GPIO.LOW:
time.sleep(0.05)
while GPIO.input(col) == GPIO.LOW:
pass
GPIO.output(row, GPIO.HIGH)
return KEYPAD_MAP[i][j]
GPIO.output(row, GPIO.HIGH)
return None
def lire_code(nb_chiffres=4, timeout=30):
saisi = ""
debut = time.time()
print(" Code : ", end="", flush=True)
while len(saisi) < nb_chiffres:
if time.time() - debut > timeout:
print("\n [Timeout]")
return ""
touche = lire_touche()
if touche and touche.isdigit():
saisi += touche
print("*", end="", flush=True)
time.sleep(0.05)
print()
return saisi
def passer_en_desarmee():
global etat, _thread_buzzer
_stop_buzzer.set()
if _thread_buzzer and _thread_buzzer.is_alive():
_thread_buzzer.join()
with etat_lock:
etat = "Désarmée"
led(b=True) # Bleu
print("[ÉTAT] ● DÉSARMÉE")
def passer_en_armee():
global etat
print("[ÉTAT] ● ARMEMENT... Stabilisation capteur (10s)")
led(r=True, g=True)
time.sleep(10)
with etat_lock:
etat = "Armée"
led(g=True)
bip(nb=2)
print("[ÉTAT] ● ARMÉE — Surveillance active !")
def passer_en_declenchee():
global etat, _thread_buzzer
with etat_lock:
etat = "Déclenchée"
led(r=True) # Rouge
print("[ÉTAT] ● DÉCLENCHÉE !")
_stop_buzzer.clear()
_thread_buzzer = threading.Thread(target=_buzzer_continu, args=(_stop_buzzer,), daemon=True)
_thread_buzzer.start()
def _surveiller_pir(stop_evt):
print("[PIR] Thread de surveillance prêt")
while not stop_evt.is_set():
with etat_lock:
etat_local = etat
if etat_local == "Armée":
if GPIO.input(PIN_PIR) == GPIO.HIGH:
time.sleep(0.3)
if GPIO.input(PIN_PIR) == GPIO.HIGH:
passer_en_declenchee()
time.sleep(0.1)
def boucle_principale():
passer_en_desarmee()
stop_pir = threading.Event()
thread_pir = threading.Thread(target=_surveiller_pir, args=(stop_pir,), daemon=True)
thread_pir.start()
print("\n=== Système prêt (Code: " + CODE_SECRET + ") ===")
try:
while True:
etat_actuel = etat_alarme()
if etat_actuel == "Désarmée":
print("→ Saisir code pour ARMER :")
if lire_code() == CODE_SECRET:
passer_en_armee()
elif etat_actuel == "Armée":
print("→ Système ARMÉ (Code pour DÉSARMER) :")
if lire_code() == CODE_SECRET:
passer_en_desarmee()
elif etat_actuel == "Déclenchée":
print("→ Saisir code pour DÉSARMER :")
if lire_code() == CODE_SECRET:
passer_en_desarmee()
time.sleep(0.2)
except KeyboardInterrupt:
print("\n[INFO] Arrêt demandé par l'utilisateur")
finally:
stop_pir.set()
_stop_buzzer.set()
led(False, False, False)
GPIO.cleanup()
print("[INFO] GPIO libérés. Fin du programme.")

View File

@@ -0,0 +1,246 @@
import time
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
class SystemeAlarme:
def __init__(self):
"""
Initialise les composants liés à l'alarme.
Cette classe gère uniquement la logique locale de sécurité :
- le capteur PIR
- le buzzer
- la LED RGB de statut
- le clavier 4x4
Elle ne dépend d'aucune autre partie du projet.
"""
# -----------------------------
# Définition des pins physiques
# -----------------------------
self.pinPir = 15
self.pinBuzzer = 18
self.pinLedRouge = 17
self.pinLedVerte = 27
self.pinLedBleue = 22
# Clavier 4x4
# 4 lignes + 4 colonnes
self.lignes = [5, 6, 13, 19]
self.colonnes = [26, 12, 16, 20]
# Disposition classique d'un clavier 4x4
self.touches = [
["1", "2", "3", "A"],
["4", "5", "6", "B"],
["7", "8", "9", "C"],
["*", "0", "#", "D"]
]
# -----------------------------
# Variables de fonctionnement
# -----------------------------
self.codeSecret = "1234"
self.codeSaisi = ""
# Etats possibles :
# - desarme
# - arme
# - alarme
self.etat = "desarme"
# Anti-rebond clavier
self.derniereLecture = 0
self.delaiLecture = 0.25
self.initialiserGPIO()
self.mettreAJourEtat()
def initialiserGPIO(self):
"""Configure les broches du Raspberry Pi pour l'alarme."""
GPIO.setup(self.pinPir, GPIO.IN)
GPIO.setup(self.pinBuzzer, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.pinLedRouge, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.pinLedVerte, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.pinLedBleue, GPIO.OUT, initial=GPIO.LOW)
for pin in self.lignes:
GPIO.setup(pin, GPIO.OUT, initial=GPIO.LOW)
for pin in self.colonnes:
GPIO.setup(pin, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
def definirCouleur(self, rouge, vert, bleu):
"""
Allume la LED RGB selon la couleur voulue.
Paramètres :
- rouge : True / False
- vert : True / False
- bleu : True / False
"""
GPIO.output(self.pinLedRouge, GPIO.HIGH if rouge else GPIO.LOW)
GPIO.output(self.pinLedVerte, GPIO.HIGH if vert else GPIO.LOW)
GPIO.output(self.pinLedBleue, GPIO.HIGH if bleu else GPIO.LOW)
def mettreAJourEtat(self):
"""
Met à jour les sorties selon l'état actuel du système.
- desarme : LED verte, buzzer éteint
- arme : LED bleue, buzzer éteint
- alarme : LED rouge, buzzer allumé
"""
if self.etat == "desarme":
self.definirCouleur(False, True, False)
GPIO.output(self.pinBuzzer, GPIO.LOW)
elif self.etat == "arme":
self.definirCouleur(False, False, True)
GPIO.output(self.pinBuzzer, GPIO.LOW)
elif self.etat == "alarme":
self.definirCouleur(True, False, False)
GPIO.output(self.pinBuzzer, GPIO.HIGH)
def armer(self):
"""Passe le système en mode armé."""
self.etat = "arme"
self.codeSaisi = ""
self.mettreAJourEtat()
print("Alarme activée.")
def desarmer(self):
"""Passe le système en mode désarmé."""
self.etat = "desarme"
self.codeSaisi = ""
self.mettreAJourEtat()
print("Alarme désactivée.")
def declencherAlarme(self):
"""
Déclenche l'alarme si un mouvement est détecté alors
que le système est armé.
"""
if self.etat != "alarme":
self.etat = "alarme"
self.codeSaisi = ""
self.mettreAJourEtat()
print("Intrusion détectée : alarme déclenchée.")
def lireClavier(self):
"""
Scanne le clavier 4x4.
Retour :
- la touche détectée
- None si aucune touche n'est pressée
"""
maintenant = time.time()
if maintenant - self.derniereLecture < self.delaiLecture:
return None
for indexLigne, ligne in enumerate(self.lignes):
GPIO.output(ligne, GPIO.HIGH)
for indexColonne, colonne in enumerate(self.colonnes):
if GPIO.input(colonne) == GPIO.HIGH:
GPIO.output(ligne, GPIO.LOW)
self.derniereLecture = maintenant
# Petite attente pour éviter la lecture multiple
time.sleep(0.05)
return self.touches[indexLigne][indexColonne]
GPIO.output(ligne, GPIO.LOW)
return None
def validerCode(self):
"""
Vérifie le code saisi.
Si le code est correct :
- alarme désarmée -> armée
- alarme armée -> désarmée
- alarme déclenchée -> désarmée
Si le code est faux :
- on efface la saisie
"""
if self.codeSaisi == self.codeSecret:
if self.etat == "desarme":
self.armer()
else:
self.desarmer()
else:
print("Code incorrect.")
self.codeSaisi = ""
def traiterClavier(self, touche):
"""
Gère la logique du clavier :
- chiffres : ajout au code saisi
- * : efface la saisie
- # : valide le code
"""
if touche is None:
return
print("Touche appuyée :", touche)
if touche == "*":
self.codeSaisi = ""
print("Saisie effacée.")
return
if touche == "#":
self.validerCode()
return
if touche.isdigit():
if len(self.codeSaisi) < 8:
self.codeSaisi += touche
print("Code en cours :", "*" * len(self.codeSaisi))
def surveillerPIR(self):
"""
Vérifie le capteur de mouvement.
Si un mouvement est détecté alors que l'alarme est armée,
on passe en état d'alarme.
"""
mouvement = GPIO.input(self.pinPir) == GPIO.HIGH
if self.etat == "arme" and mouvement:
self.declencherAlarme()
def mettreAJour(self):
"""
Fonction appelée en boucle dans le programme principal.
Elle :
- lit le clavier
- traite la touche appuyée
- surveille le PIR
- synchronise LED et buzzer avec l'état courant
"""
touche = self.lireClavier()
self.traiterClavier(touche)
self.surveillerPIR()
self.mettreAJourEtat()
def cleanup(self):
"""
Remet les sorties dans un état propre à la fermeture.
"""
GPIO.output(self.pinBuzzer, GPIO.LOW)
self.definirCouleur(False, False, False)

View File

@@ -0,0 +1,24 @@
import time
import threading
import alarme
from porterfid import SystemePorteRFID
import RPi.GPIO as GPIO
porte = SystemePorteRFID()
def call_board1():
thread_alarme = threading.Thread(target=alarme.boucle_principale, daemon=True)
thread_alarme.start()
print("[BOARD 1] Système d'alarme lancé en arrière-plan.")
try:
while True:
porte.mettreAJour()
time.sleep(0.3)
except KeyboardInterrupt:
print("\nArrêt manuel du programme.")
finally:
porte.cleanup()
GPIO.cleanup()

View File

@@ -0,0 +1,32 @@
import time
from thermostat import SystemeThermostat
from lumieres import SystemeLumieres
from volets import SystemeVolets
from etatsysteme import EtatSysteme
thermostat = SystemeThermostat()
lumieres = SystemeLumieres()
volets = SystemeVolets()
etat = EtatSysteme()
try:
while True:
erreurThermostat = thermostat.mettreAJour()
erreurLumieres = lumieres.mettreAJour()
erreurVolets = volets.mettreAJour()
if erreurThermostat or erreurLumieres or erreurVolets:
etat.signalerProbleme()
else:
etat.signalerOk()
time.sleep(0.2)
except KeyboardInterrupt:
print("\nArrêt du programme.")
finally:
thermostat.cleanup()
lumieres.cleanup()
volets.cleanup()
etat.cleanup()

View File

@@ -0,0 +1,46 @@
import RPi.GPIO as GPIO
import time as t
from septsegments import afficher_temperature
from DHT11 import lire_temperature
# --- Configuration des Pins ---
bouton_up = 23
bouton_down = 24
# --- Variables Globales Partagées ---
temperature_cible = 18
def setup_boutons():
"""Initialisation des GPIO (à appeler au début)"""
GPIO.setmode(GPIO.BCM)
GPIO.setup(bouton_up, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(bouton_down, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def test_boutons():
global temperature_cible
setup_boutons()
temperature_DHT = lire_temperature()
etatPrecedent_up = GPIO.input(bouton_up)
etatPrecedent_down = GPIO.input(bouton_down)
print(f"Thermostat lancé ! Cible actuelle : {temperature_cible}°C")
while True:
etat_up = GPIO.input(bouton_up)
etat_down = GPIO.input(bouton_down)
if etat_up == 0 and etatPrecedent_up == 1:
temperature_cible = min(40, temperature_cible + 1)
print(f"Bouton UP -> Nouvelle cible : {temperature_cible}")
afficher_temperature(lire_temperature(), temperature_cible)
if etat_down == 0 and etatPrecedent_down == 1:
temperature_cible = max(0, temperature_cible - 1)
print(f"Bouton DOWN -> Nouvelle cible : {temperature_cible}")
afficher_temperature(lire_temperature(), temperature_cible)
etatPrecedent_up = etat_up
etatPrecedent_down = etat_down
t.sleep(0.05)

View File

@@ -0,0 +1,60 @@
import RPi.GPIO as GPIO
import time as t
servo_pin = 18
bouton_up = 27
bouton_down = 16
etat_porte = "Fermé"
def get_etat(etat):
global etat_porte
etat_porte = etat
print(f"État mis à jour : {etat_porte}")
def test_boutons():
global etat_porte
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(servo_pin, GPIO.OUT)
GPIO.setup(bouton_up, GPIO.IN, pull_up_down=GPIO.PUD_UP)
GPIO.setup(bouton_down, GPIO.IN, pull_up_down=GPIO.PUD_UP)
pwm = GPIO.PWM(servo_pin, 50)
pwm.start(0)
etatPrecedent_up = GPIO.input(bouton_up)
etatPrecedent_down = GPIO.input(bouton_down)
print("Thread Servo : Démarré et prêt.")
try:
while True:
etat_up = GPIO.input(bouton_up)
etat_down = GPIO.input(bouton_down)
if etat_up != etatPrecedent_up:
if etat_up == GPIO.LOW:
get_etat('Ouvert')
pwm.ChangeDutyCycle(2.5)
t.sleep(0.5)
pwm.ChangeDutyCycle(0)
etatPrecedent_up = etat_up
if etat_down != etatPrecedent_down:
if etat_down == GPIO.LOW:
get_etat('Fermé')
pwm.ChangeDutyCycle(7.5)
t.sleep(0.5)
pwm.ChangeDutyCycle(0)
etatPrecedent_down = etat_down
t.sleep(0.05)
except Exception as e:
print(f"Erreur dans le thread servo : {e}")
finally:
pwm.stop()
#test_boutons()

View File

@@ -0,0 +1,25 @@
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
class EtatSysteme:
def __init__(self):
self.pinLedRouge = 19
self.pinLedVerte = 26
GPIO.setup(self.pinLedRouge, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.pinLedVerte, GPIO.OUT, initial=GPIO.LOW)
def signalerOk(self):
GPIO.output(self.pinLedVerte, GPIO.HIGH)
GPIO.output(self.pinLedRouge, GPIO.LOW)
def signalerProbleme(self):
GPIO.output(self.pinLedVerte, GPIO.LOW)
GPIO.output(self.pinLedRouge, GPIO.HIGH)
def cleanup(self):
GPIO.output(self.pinLedRouge, GPIO.LOW)
GPIO.output(self.pinLedVerte, GPIO.LOW)

View File

@@ -0,0 +1,15 @@
import RPi.GPIO as GPIO
import time as t
GPIO.setmode(GPIO.BOARD)
r, g, b = 11, 13, 15
GPIO.setup(r, GPIO.OUT)
GPIO.setup(g, GPIO.OUT)
GPIO.setup(b, GPIO.OUT)
while True:
GPIO.output(r, 1); t.sleep(1); GPIO.output(r, 0)
GPIO.output(g, 1); t.sleep(1); GPIO.output(g, 0)
GPIO.output(b, 1); t.sleep(1); GPIO.output(b, 0)

View File

@@ -0,0 +1,87 @@
import time
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
class SystemeLumieres:
def __init__(self):
# la pin de la 2e photorésistance manque sur le schéma
# donc ici on met une valeur temporaire
# elle apparaîtra peut-être un jour, comme la motivation en fin de projet
self.pinPhotoInterieure = 29
self.led1 = 6
self.led2 = 9
self.led3 = 13
self.boutonManuel = 36
self.modeManuel = False
self.lumieresAllumees = False
GPIO.setup(self.pinPhotoInterieure, GPIO.IN)
GPIO.setup(self.led1, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.led2, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.led3, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.boutonManuel, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
self.derniereLectureBouton = 0
self.delaiLectureBouton = 0.25
def lireLuminositeInterieure(self):
return GPIO.input(self.pinPhotoInterieure)
def allumerLumieres(self):
GPIO.output(self.led1, GPIO.HIGH)
GPIO.output(self.led2, GPIO.HIGH)
GPIO.output(self.led3, GPIO.HIGH)
self.lumieresAllumees = True
def eteindreLumieres(self):
GPIO.output(self.led1, GPIO.LOW)
GPIO.output(self.led2, GPIO.LOW)
GPIO.output(self.led3, GPIO.LOW)
self.lumieresAllumees = False
def gererBoutonManuel(self):
maintenant = time.time()
if maintenant - self.derniereLectureBouton < self.delaiLectureBouton:
return
if GPIO.input(self.boutonManuel):
self.modeManuel = not self.modeManuel
self.derniereLectureBouton = maintenant
if self.modeManuel:
self.lumieresAllumees = not self.lumieresAllumees
if self.lumieresAllumees:
self.allumerLumieres()
else:
self.eteindreLumieres()
else:
print("Lumières : retour en auto")
def mettreAJour(self):
self.gererBoutonManuel()
if self.modeManuel:
return False
luminosite = self.lireLuminositeInterieure()
if luminosite == 0:
self.allumerLumieres()
print("Lumières : on allume")
else:
self.eteindreLumieres()
print("Lumières : on coupe")
return False
def cleanup(self):
self.eteindreLumieres()

View File

@@ -0,0 +1,91 @@
import time
import threading
import RPi.GPIO as GPIO
from mfrc522 import SimpleMFRC522
import requests
import urllib3
# On cache le gros texte d'avertissement orange (InsecureRequestWarning)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
class SystemePorteRFID:
def __init__(self):
"""
Initialise le système local d'accès par RFID.
Gère le lecteur RFID et la LED de la porte.
L'authentification est maintenant gérée par le serveur Flask et MariaDB.
"""
self.pinLed = 4
GPIO.setup(self.pinLed, GPIO.OUT, initial=GPIO.LOW)
self.lecteur = SimpleMFRC522()
self.badgeDetecte = None
self.derniereOuverture = 0
self.delaiEntreScans = 2
# Ce thread sert à ne pas bloquer la boucle principale
self.threadLecture = threading.Thread(target=self.boucleLectureRFID, daemon=True)
self.threadLecture.start()
def boucleLectureRFID(self):
"""
Boucle secondaire qui attend les badges RFID.
Utilise read_id() au lieu de read() pour éviter les erreurs "AUTH ERROR"
sur la mémoire interne des badges.
"""
while True:
try:
badgeId = self.lecteur.read_id()
self.badgeDetecte = badgeId
except Exception as erreur:
print("Erreur RFID :", erreur)
time.sleep(1)
def ouvrirPorte(self):
"""Simule l'ouverture de la porte avec la LED pendant 2 secondes."""
print("Porte ouverte.")
GPIO.output(self.pinLed, GPIO.HIGH)
time.sleep(2)
GPIO.output(self.pinLed, GPIO.LOW)
def traiterBadge(self, badgeId):
print(f"Badge détecté : {badgeId}")
try:
url = "https://127.0.0.1/rfid-scan"
donnees = {"badge_id": str(badgeId)}
reponse = requests.post(url, json=donnees, timeout=2, verify=False)
data = reponse.json()
if data.get("success") is True:
nom_utilisateur = data.get("username")
print(f"Accès autorisé par la base de données pour : {nom_utilisateur}")
self.ouvrirPorte()
else:
print("Accès refusé : ce badge n'est assigné à personne dans la base de données.")
except Exception as e:
print("Erreur de communication avec Flask :", e)
def mettreAJour(self):
"""Fonction appelée en boucle dans le programme principal."""
if self.badgeDetecte is None:
return
if time.time() - self.derniereOuverture < self.delaiEntreScans:
return
badgeId = self.badgeDetecte
self.badgeDetecte = None
self.derniereOuverture = time.time()
self.traiterBadge(badgeId)
def cleanup(self):
"""Eteint la LED de porte lors de la fermeture du programme."""
GPIO.output(self.pinLed, GPIO.LOW)

View File

@@ -0,0 +1,32 @@
import tm1637
import time as t
_display = None
def get_display():
global _display
if _display is None:
_display = tm1637.TM1637(clk=4, dio=17)
_display.brightness(2)
return _display
def afficher_temperature(temperature, temperature_moyenne):
print(f"Test affichage: Cible {temperature} : Moyenne {temperature_moyenne}")
try:
temp1 = int(temperature)
temp2 = int(temperature_moyenne)
disp = get_display()
texte_ecran = f"{temp1:02d}{temp2:02d}"
if hasattr(disp, 'show_doublepoint'):
disp.show_doublepoint(True)
elif hasattr(disp, 'point'):
disp.point(True)
disp.show(texte_ecran)
except Exception as e:
print(f"Erreur d'affichage : {e}")

View File

@@ -0,0 +1,76 @@
import time
import RPi.GPIO as GPIO
import Adafruit_DHT
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
class SystemeThermostat:
def __init__(self):
self.capteur = Adafruit_DHT.DHT11
self.pinDht = 25
self.boutonPlus = 16
self.boutonMoins = 18
# display 4 digits
# à brancher proprement quand le module exact sera fixé
self.pinDisplayClk = 7
self.pinDisplayDio = 11
self.temperatureCible = 18
self.temperatureReelle = None
self.derniereLectureBouton = 0
self.delaiLectureBouton = 0.25
GPIO.setup(self.boutonPlus, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
GPIO.setup(self.boutonMoins, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
def lireTemperature(self):
humidite, temperature = Adafruit_DHT.read_retry(self.capteur, self.pinDht)
if temperature is None:
print("Thermostat : lecture DHT11 impossible")
return None
return temperature
def lireBoutons(self):
maintenant = time.time()
if maintenant - self.derniereLectureBouton < self.delaiLectureBouton:
return
if GPIO.input(self.boutonPlus):
self.temperatureCible += 1
self.derniereLectureBouton = maintenant
print("Consigne :", self.temperatureCible, "°C")
elif GPIO.input(self.boutonMoins):
self.temperatureCible -= 1
self.derniereLectureBouton = maintenant
print("Consigne :", self.temperatureCible, "°C")
def afficherTemperatures(self):
# pour l'instant on affiche dans la console
# le vrai code du display ira ici
if self.temperatureReelle is None:
print("Temp réelle : -- | cible :", self.temperatureCible)
else:
print("Temp réelle :", str(self.temperatureReelle) + "°C", "| cible :", str(self.temperatureCible) + "°C")
def mettreAJour(self):
self.lireBoutons()
self.temperatureReelle = self.lireTemperature()
self.afficherTemperatures()
if self.temperatureReelle is None:
return True
return False
def cleanup(self):
# rien à éteindre ici, incroyable mais vrai
pass

View File

@@ -0,0 +1,73 @@
import time
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BOARD)
GPIO.setwarnings(False)
class SystemeVolets:
def __init__(self):
self.pinPhotoExterieure = 32
self.pinServo = 12
self.boutonManuel = 13
self.modeManuel = False
self.voletsOuverts = True
GPIO.setup(self.pinPhotoExterieure, GPIO.IN)
GPIO.setup(self.pinServo, GPIO.OUT)
GPIO.setup(self.boutonManuel, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
self.pwm = GPIO.PWM(self.pinServo, 50)
self.pwm.start(0)
self.derniereLectureBouton = 0
self.delaiLectureBouton = 0.25
def ouvrirVolets(self):
self.pwm.ChangeDutyCycle(7)
self.voletsOuverts = True
print("Volets : ouverts")
def fermerVolets(self):
self.pwm.ChangeDutyCycle(2)
self.voletsOuverts = False
print("Volets : fermés")
def gererBoutonManuel(self):
maintenant = time.time()
if maintenant - self.derniereLectureBouton < self.delaiLectureBouton:
return
if GPIO.input(self.boutonManuel):
self.derniereLectureBouton = maintenant
if self.voletsOuverts:
self.fermerVolets()
else:
self.ouvrirVolets()
self.modeManuel = True
print("Volets : mode manuel")
def lireLuminositeExterieure(self):
return GPIO.input(self.pinPhotoExterieure)
def mettreAJour(self):
self.gererBoutonManuel()
if self.modeManuel:
return False
luminosite = self.lireLuminositeExterieure()
if luminosite == 1:
self.fermerVolets()
else:
self.ouvrirVolets()
return False
def cleanup(self):
self.pwm.stop()

View File

@@ -1,11 +0,0 @@
from gpiozero import LED
from time import sleep
led = LED(17)
while True:
led.on()
sleep(1)
led.off()
sleep(1)

View File

@@ -1,52 +0,0 @@
"""
code adapté un une utilisation avec mcp3008 pour ldr
"""
import time
from gpiozero import LED, Button, PWMOutputDevice, AngularServo
from gpiozero import MCP3008
led_verte = LED(12)
led_verte_luminosite = LED(25)
led_rouge = LED(13)
led_bleue = LED(14)
led_rouge_gas = LED(10)
servo = AngularServo(32, min_angle=0, max_angle=180)
pir_sensor = Button(33)
gas_sensor = Button(34)
buzzer = PWMOutputDevice(11)
ldr_sensor = MCP3008(channel=0)
def activate_alarm():
for _ in range(3):
buzzer.value = 0.5
time.sleep(0.5)
buzzer.value = 0
time.sleep(0.5)
def pir_detection():
while True:
if pir_sensor.is_pressed:
led_bleue.on()
time.sleep(3)
led_bleue.off()
time.sleep(0.1)
def luminosite_detection():
while True:
luminosite = ldr_sensor.value * 1023
print(luminosite)
if luminosite > 300:
led_verte_luminosite.on()
else:
led_verte_luminosite.off()
time.sleep(0.5)
import threading
threading.Thread(target=pir_detection, daemon=True).start()
threading.Thread(target=luminosite_detection, daemon=True).start()

View File

@@ -1,12 +0,0 @@
from machine import Pin
pir_sensor = Pin(33, Pin.IN)
def pir_detection():
while True:
if pir_sensor.value() == 1:
led_bleue.on()
utime.sleep(3)
led_bleue.off()
utime.sleep(0.1)

205
composants/test/ALARM_V2.py Normal file
View File

@@ -0,0 +1,205 @@
import RPi.GPIO as GPIO
import time
import threading
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
PIN_LED_R = 17
PIN_LED_G = 22
PIN_LED_B = 27
PIN_PIR = 15
PIN_BUZZER = 18
ROWS = [5, 6, 13, 19]
COLS = [26, 12, 16, 20]
KEYPAD_MAP = [
['1', '2', '3', 'A'],
['4', '5', '6', 'B'],
['7', '8', '9', 'C'],
['*', '0', '#', 'D'],
]
CODE_SECRET = "1234"
GPIO.setup(PIN_LED_R, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PIN_LED_G, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PIN_LED_B, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PIN_BUZZER, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(PIN_PIR, GPIO.IN)
for row in ROWS:
GPIO.setup(row, GPIO.OUT, initial=GPIO.HIGH)
for col in COLS:
GPIO.setup(col, GPIO.IN, pull_up_down=GPIO.PUD_UP)
etat = "desarmee"
etat_lock = threading.Lock()
_stop_buzzer = threading.Event()
_thread_buzzer = None
def led(r=False, g=False, b=False):
"""Allume la LED RGB avec la couleur voulue."""
GPIO.output(PIN_LED_R, GPIO.HIGH if r else GPIO.LOW)
GPIO.output(PIN_LED_G, GPIO.HIGH if g else GPIO.LOW)
GPIO.output(PIN_LED_B, GPIO.HIGH if b else GPIO.LOW)
def led_bleu(): led(b=True)
def led_vert(): led(g=True)
def led_rouge(): led(r=True)
def led_off(): led()
def bip(nb=1, duree=0.08, pause=0.12):
"""Émet nb bip(s) courts."""
for _ in range(nb):
GPIO.output(PIN_BUZZER, GPIO.HIGH)
time.sleep(duree)
GPIO.output(PIN_BUZZER, GPIO.LOW)
time.sleep(pause)
def _buzzer_continu(stop_event: threading.Event):
"""Boucle interne : buzzer ON/OFF jusqu'à stop_event."""
while not stop_event.is_set():
GPIO.output(PIN_BUZZER, GPIO.HIGH)
time.sleep(0.5)
GPIO.output(PIN_BUZZER, GPIO.LOW)
time.sleep(0.5)
GPIO.output(PIN_BUZZER, GPIO.LOW)
def lire_touche():
"""
Scan matriciel : met chaque ligne à LOW tour à tour
et lit les colonnes. Retourne la touche ou None.
"""
for i, row in enumerate(ROWS):
GPIO.output(row, GPIO.LOW)
for j, col in enumerate(COLS):
if GPIO.input(col) == GPIO.LOW:
time.sleep(0.05)
while GPIO.input(col) == GPIO.LOW:
pass
GPIO.output(row, GPIO.HIGH)
return KEYPAD_MAP[i][j]
GPIO.output(row, GPIO.HIGH)
return None
def lire_code(timeout=30):
saisi = ""
debut = time.time()
print(" Code (# pour valider, * pour effacer) : ", end="", flush=True)
while True:
if time.time() - debut > timeout:
print("\n [Timeout — saisie annulée]")
return ""
touche = lire_touche()
if touche is None:
time.sleep(0.05)
continue
if touche == '#':
print()
return saisi
elif touche == '*':
if saisi:
saisi = saisi[:-1]
print("\b \b", end="", flush=True)
elif touche.isdigit():
saisi += touche
print("*", end="", flush=True)
time.sleep(0.05)
def passer_en_desarmee():
global etat, _thread_buzzer
_stop_buzzer.set()
if _thread_buzzer and _thread_buzzer.is_alive():
_thread_buzzer.join()
with etat_lock:
etat = "desarmee"
led_bleu()
print("[ÉTAT] ● DÉSARMÉE — LED bleue")
def passer_en_armee():
global etat
with etat_lock:
etat = "armee"
led_vert()
bip(nb=2)
print("[ÉTAT] ● ARMÉE — LED verte — PIR actif")
def passer_en_declenchee():
global etat, _thread_buzzer
with etat_lock:
etat = "declenchee"
led_rouge()
print("[ÉTAT] ● DÉCLENCHÉE — LED rouge — buzzer actif")
_stop_buzzer.clear()
_thread_buzzer = threading.Thread(
target=_buzzer_continu, args=(_stop_buzzer,), daemon=True
)
_thread_buzzer.start()
def _surveiller_pir(stop_evt: threading.Event):
"""Lit le PIR toutes les 100 ms. Déclenche si mouvement et armée."""
print("[PIR] Surveillance démarrée")
while not stop_evt.is_set():
with etat_lock:
etat_local = etat
if etat_local == "armee" and GPIO.input(PIN_PIR) == GPIO.HIGH:
print("[PIR] ⚠ Mouvement détecté !")
passer_en_declenchee()
time.sleep(0.1)
def boucle_principale():
global etat
passer_en_desarmee()
stop_pir = threading.Event()
thread_pir = threading.Thread(
target=_surveiller_pir, args=(stop_pir,), daemon=True
)
thread_pir.start()
print("\n=== Système d'alarme démarré ===")
print(" Tapez le code sur le keypad pour armer / désarmer.\n")
try:
while True:
with etat_lock:
etat_local = etat
if etat_local == "desarmee":
print(" → Saisir le code pour ARMER :")
code = lire_code() # CORRECTION ICI
if code == CODE_SECRET:
print(" ✔ Code correct → armement")
passer_en_armee()
elif code != "":
print(" ✘ Code incorrect")
bip(nb=1, duree=0.4)
elif etat_local == "armee":
time.sleep(0.1)
elif etat_local == "declenchee":
print(" → Saisir le code pour DÉSARMER :")
code = lire_code() # CORRECTION ICI
if code == CODE_SECRET:
print(" ✔ Code correct → désarmement")
passer_en_desarmee() # AJOUT : pour que l'alarme s'arrête vraiment
elif code != "":
print(" ✘ Code incorrect — alarme maintenue")
except KeyboardInterrupt:
print("\n[INFO] Arrêt demandé (Ctrl+C)")
finally:
stop_pir.set()
_stop_buzzer.set()
led_off()
GPIO.cleanup()
print("[INFO] GPIO libérés. Fin du programme.")
if __name__ == "__main__":
boucle_principale()

15
composants/test/PIR.py Normal file
View File

@@ -0,0 +1,15 @@
import RPi.GPIO as GPIO
import time as t
##
GPIO.setmode(GPIO.BOARD)
pir = 10
GPIO.setup(pir, GPIO.IN)
print("Stabilisation...")
t.sleep(10)
print("Stabilisation terminée")
while True:
print("PIR :", GPIO.input(pir))
t.sleep(0.5)

View File

@@ -0,0 +1,36 @@
import RPi.GPIO as GPIO
import time as t
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
led1 = 9
led2 = 6
led3 = 13 # example BCM pin
GPIO.setup(led1, GPIO.OUT)
GPIO.setup(led2, GPIO.OUT)
GPIO.setup(led3, GPIO.OUT)
print("Test LEDs...")
try:
while True:
GPIO.output(led1, GPIO.HIGH)
GPIO.output(led2, GPIO.HIGH)
GPIO.output(led3, GPIO.HIGH)
print("LED ON")
t.sleep(1)
GPIO.output(led1, GPIO.LOW)
GPIO.output(led2, GPIO.LOW)
GPIO.output(led3, GPIO.LOW)
print("LED OFF")
t.sleep(1)
except KeyboardInterrupt:
print("Stop")
finally:
GPIO.cleanup()

15
composants/test/ledRGB.py Normal file
View File

@@ -0,0 +1,15 @@
import RPi.GPIO as GPIO
import time as t
##
GPIO.setmode(GPIO.BOARD)
r, g, b = 11, 13, 15
GPIO.setup(r, GPIO.OUT)
GPIO.setup(g, GPIO.OUT)
GPIO.setup(b, GPIO.OUT)
while True:
GPIO.output(r, 1); t.sleep(1); GPIO.output(r, 0)
GPIO.output(g, 1); t.sleep(1); GPIO.output(g, 0)
GPIO.output(b, 1); t.sleep(1); GPIO.output(b, 0)

15
composants/test/rfid.py Normal file
View File

@@ -0,0 +1,15 @@
from mfrc522 import SimpleMFRC522 as RFIDReader
##
reader = RFIDReader()
print("En attente...")
try:
while True:
badgeId, text = reader.read()
print("ID :", badgeId)
print("Texte :", text)
print("-----")
except KeyboardInterrupt:
print("Stop")

19
composants/test/servo.py Normal file
View File

@@ -0,0 +1,19 @@
import RPi.GPIO as GPIO
import time as t
##
GPIO.setmode(GPIO.BOARD)
servo = 12
GPIO.setup(servo, GPIO.OUT)
pwm = GPIO.PWM(servo, 50)
pwm.start(0)
while True:
print("Position 1")
pwm.ChangeDutyCycle(2)
t.sleep(2)
print("Position 2")
pwm.ChangeDutyCycle(7)
t.sleep(2)

79
fastapi/README.md Normal file
View File

@@ -0,0 +1,79 @@
🏠 Projet IoT : API de Contrôle Domotique (Raspberry Pi 2)
Ce dépôt contient le code du Serveur API déployé sur le deuxième Raspberry Pi. Son rôle est de piloter les composants physiques (LED, Thermostat, Afficheurs) en répondant aux requêtes envoyées par le Raspberry Pi 1.
🏗️ Architecture du Système
Le projet repose sur une architecture distribuée :
Raspberry Pi 1 (Client) : Envoie les requêtes HTTP/HTTPS.
Raspberry Pi 2 (Serveur - Ce repo) : Reçoit les ordres, exécute la logique métier via les GPIO et renvoie l'état du système.
🛠️ Installation Rapide
Un script d'automatisation est fourni pour configurer l'environnement, les bases de données et les dépendances.
Bash
# 1. Rendre le script exécutable
chmod +x main.sh
# 2. Lancer l'installation (nécessite les droits sudo)
sudo ./main.sh
Ce que fait le script main.sh :
Mise à jour du système (apt update & upgrade).
Installation optionnelle de MariaDB et phpMyAdmin.
Configuration d'un environnement virtuel Python (venv).
Installation des dépendances via requirement.txt.
🚀 Lancement du Serveur
Pour démarrer l'API, utilisez le script de lancement qui vérifie les prérequis (Python, SSL, Avahi) avant de lancer Uvicorn :
Bash
chmod +x run_api.sh
sudo ./run_api.sh
Note : Le serveur utilise HTTPS. Les certificats doivent être présents dans ../web_secu/ssl/.
📡 Points d'entrée de l'API (Endpoints)
L'API est documentée automatiquement (Swagger) via FastAPI à l'adresse : https://<IP_DU_PI>:8000/docs
Méthode Route Description
GET /up_led Allume les lumières et active le mode manuel.
GET /down_led Éteint les lumières et active le mode manuel.
GET /temperature Lit le capteur DHT11 et affiche la valeur sur le 7 segments.
📂 Structure du Code Python (main.py)
Le serveur utilise une approche modulaire pour gérer les composants :
SystemeLumieres : Gestion des LEDs.
SystemeThermostat : Lecture des données capteurs.
EtatSysteme : Gestion visuelle des erreurs/succès (LED d'état).
afficher_temperature : Pilotage de l'afficheur TM1637.
📋 Dépendances Principales
FastAPI & Uvicorn : Framework web et serveur ASGI.
RPi.GPIO : Contrôle des broches du Raspberry Pi.
Adafruit_DHT : Lecture des capteurs d'humidité et température.
python-tm1637 : Driver pour l'afficheur 7 segments.
⚠️ Notes de Sécurité
Le serveur est configuré avec CORS autorisant toutes les origines ("*") pour faciliter le développement.
La communication est sécurisée par SSL (TLS).
Le service Avahi est utilisé pour la résolution de noms sur le réseau local.
API Développée par les loustiques

106
fastapi/main.py Normal file
View File

@@ -0,0 +1,106 @@
import os
import sys
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import RPi.GPIO as GPIO
import uvicorn
import threading
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
composants = os.path.join(BASE_DIR, "composants", "byPanda")
sys.path.insert(0, composants)
from lumieres import SystemeLumieres
from thermostat import SystemeThermostat
#from volets import SystemeVolets
from etatsysteme import EtatSysteme
from septsegments import afficher_temperature
import bouton_servo
import bouton
import LDR
app = FastAPI(title="L'API des loustiques")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=False,
allow_methods=["*"],
allow_headers=["*"],
)
controleur_lumieres = SystemeLumieres()
controleur_thermostat = SystemeThermostat()
#controleur_volet = SystemeVolets()
etatSysteme = EtatSysteme()
@app.get("/up_led")
async def allumer_led():
try:
controleur_lumieres.allumerLumieres()
controleur_lumieres.modeManuel = True
etatSysteme.signalerOk()
return {"success": True, "message": "Lumière allumée par le Pi 2"}
except Exception as e:
etatSysteme.signalerProbleme()
return {"success": False, "message": str(e)}
@app.get("/down_led")
async def eteindre_led():
try:
controleur_lumieres.eteindreLumieres()
controleur_lumieres.modeManuel = True
etatSysteme.signalerOk()
return {"success": True, "message": "Lumière éteinte par le Pi 2"}
except Exception as e:
etatSysteme.signalerProbleme()
return {"success": False, "message": str(e)}
@app.get("/temperature")
async def read_temp():
try:
temp = controleur_thermostat.lireTemperature()
if temp is None:
# On renvoie une valeur par défaut ou 0 pour éviter le undefined
return {"success": False, "temperature": "--", "message": "Erreur DHT11"}
afficher_temperature(temp, bouton.temperature_cible)
return {"success": True, "temperature": temp}
except Exception as e:
return {"success": False, "temperature": "Erreur", "message": str(e)}
@app.get("/volet_status")
async def volet_status():
try:
actuel = bouton_servo.etat_porte
return {"success": True, "status": actuel}
except Exception as e:
return {"success": False, "message": str(e)}
@app.get("/luminosite")
def get_luminosite():
return ({"success": True, "status": LDR.lire_etat()})
if __name__ == "__main__":
t1 = threading.Thread(target=bouton_servo.test_boutons, daemon=True)
t2 = threading.Thread(target=bouton.test_boutons, daemon=True)
t1.start()
t2.start()
chemin_cle = os.path.join(BASE_DIR, 'web_secu', 'ssl', 'key.pem')
chemin_cert = os.path.join(BASE_DIR, 'web_secu', 'ssl', 'cert.pem')
uvicorn.run(
"main:app",
host="0.0.0.0",
port=8000,
ssl_keyfile=chemin_cle,
ssl_certfile=chemin_cert
)

183
fastapi/main.sh Executable file
View File

@@ -0,0 +1,183 @@
#!/bin/bash
cat << 'EOF'
_______ ________ ___ ___ _______ ________
|\ ___ \ |\ __ \|\ \|\ \|\ ___ \ |\ ____\
\ \ __/|\ \ \|\ \ \ \\\ \ \ __/|\ \ \___|
\ \ \_|/_\ \ ____\ \ __ \ \ \_|/_\ \ \
\ \ \_|\ \ \ \___|\ \ \ \ \ \ \_|\ \ \ \____
\ \_______\ \__\ \ \__\ \__\ \_______\ \_______\
\|_______|\|__| \|__|\|__|\|_______|\|_______|
EOF
# ==============================================
# Script de configuration automatique Raspberry Pi - Projet IoT
# ==============================================
set -euo pipefail
SEPARATOR="=============================================="
print_step() {
echo ""
echo "$SEPARATOR"
echo " $1"
echo "$SEPARATOR"
}
# Vérification des droits sudo
if [ "$EUID" -ne 0 ]; then
echo " Ce script doit être exécuté avec sudo"
echo " Utilisation : sudo ./main.sh"
exit 1
fi
print_step " Lancement du programme de configuration IoT"
print_step " Lancement du programme de configuration IoT"
sleep 1
# ----------------------------
# 1. Mise à jour du système
# ----------------------------
print_step " Mise à jour du système (apt update & upgrade)"
print_step " Mise à jour du système (apt update & upgrade)"
if ! apt update && apt upgrade -y; then
echo " Erreur lors de la mise à jour du système"
exit 1
fi
echo "Système mis à jour"
sleep 1
#----------------------------
# 2. Installation de MariaDB x phpmyadmin
#------------------------------
read -p "Installer mariadb et phpMyAdmin ? (y/n)" db
if [ "$db" = 'y'];then
bash DB/main.sh
fi
#-------------------------------
# 3. Exportation des DB
#--------------------------------
read -p "Voulez-vous exporter des DB existantes ? (y/n) : " choix
if [ "$choix" = "y" ]; then
bash "$(dirname "$0")/export_db.sh"
fi
echo 'je sais ya un problème de numéro mais wola aller lire le script db/export_db.sh je suis claquax au sol sur terre'
# ----------------------------
# 2. Installation de Python
# ----------------------------
print_step "Vérification / Installation de Python3"
if ! apt install python3 python3-pip python3-venv -y; then
echo "Erreur lors de l'installation de Python3"
exit 1
fi
PYTHON_VERSION=$(python3 --version 2>&1)
echo " $PYTHON_VERSION installé"
sleep 1
# ----------------------------
# 3. Recherche des venvs existants
# ----------------------------
print_step "Recherche des environnements virtuels (venv) existants..."
SEARCH_DIRS=("$(pwd)")
VENV_LIST=()
for dir in "${SEARCH_DIRS[@]}"; do
if [ -d "$dir" ]; then
while IFS= read -r -d '' activate_path; do
venv_dir=$(dirname "$(dirname "$activate_path")")
if [ -f "$venv_dir/bin/python" ]; then
VENV_LIST+=("$venv_dir")
fi
done < <(find "$dir" -name "activate" -path "*/bin/activate" 2>/dev/null -print0)
fi
done
echo ""
if [ ${#VENV_LIST[@]} -eq 0 ]; then
echo " Aucun environnement virtuel trouvé dans : $(pwd)"
else
echo "${#VENV_LIST[@]} environnement(s) virtuel(s) trouvé(s) :"
for i in "${!VENV_LIST[@]}"; do
venv="${VENV_LIST[$i]}"
python_ver=$("$venv/bin/python" --version 2>&1)
echo ""
echo " [$((i+1))] Chemin : $venv"
echo " Python : $python_ver"
echo " ▶ Activer : source $venv/bin/activate"
done
SELECTED_VENV="${VENV_LIST[0]}"
echo "$SELECTED_VENV" > ./.venv_path
echo ""
echo "Venv sélectionné et enregistré : $SELECTED_VENV"
fi
# ----------------------------
# 4. Créer un nouveau venv ?
# ----------------------------
print_step " Créer un nouvel environnement virtuel ?"
echo "Voulez-vous créer un nouveau venv ? (o/n)"
read -r CREATE_VENV
if [[ "$CREATE_VENV" =~ ^[oO]$ ]]; then
VENV_PATH="$SEARCH_DIRS/venv"
if python3 -m venv $SEARCH_DIRS/venv; then
echo ""
echo "Venv créé avec succès !"
echo " Chemin : $VENV_PATH"
echo " ▶ Activer : source $VENV_PATH/bin/activate"
echo "$VENV_PATH" > ./.venv_path
echo "Chemin enregistré dans .venv_path"
"$VENV_PATH/bin/pip" install --upgrade pip
if [ -f "./requirements.txt" ]; then
echo "Installation des dépendances depuis requirements.txt..."
"$VENV_PATH/bin/pip" install -r ./requirements.txt
echo " Dépendances installées"
else
echo " Aucun requirements.txt trouvé, installation des dépendances ignorée"
fi
else
echo " Erreur lors de la création du venv à : $VENV_PATH"
exit 1
fi
else
echo " Création ignorée"
fi
# ----------------------------
# Fin
# ----------------------------
print_step " Configuration terminée"
echo ""
if [ -f "./.venv_path" ]; then
echo "Venv configuré : $(cat ./.venv_path)"
echo " Pour l'activer manuellement : source $(cat ./.venv_path)/bin/activate"
else
echo " Aucun venv enregistré — relancez le script et créez un venv"
fi
echo ""

5
fastapi/requirement.txt Normal file
View File

@@ -0,0 +1,5 @@
fastapi
uvicorn
rpi.gpio
python-tm1637
Adafruit_DHT

69
fastapi/run_api.sh Executable file
View File

@@ -0,0 +1,69 @@
#!/bin/bash
cat << 'EOF'
=============================
_______ ________ ___ ___ _______ ________
|\ ___ \ |\ __ \|\ \|\ \|\ ___ \ |\ ____\
\ \ __/|\ \ \|\ \ \ \\\ \ \ __/|\ \ \___|
\ \ \_|/_\ \ ____\ \ __ \ \ \_|/_\ \ \
\ \ \_|\ \ \ \___|\ \ \ \ \ \ \_|\ \ \ \____
\ \_______\ \__\ \ \__\ \__\ \_______\ \_______\
\|_______|\|__| \|__|\|__|\|_______|\|_______|
=============================
EOF
cat << 'EOF'
===============================
Vérification de la présence de python
=================================
EOF
VERSION_PYTHON=$(python3 --version 2>&1)
if [ $? -eq 0 ]; then
echo "Python est installé"
echo "Voici la version : $VERSION_PYTHON"
else
apt install -y python3
fi
cat << 'EOF'
===================================================
Vérification de la présence de la bibliothèque FLASK
====================================================
EOF
if venv/bin/python -m pip list | grep -qi 'flask'; then
echo "Flask existe bien"
else
echo "Flask n'est pas installé..."
echo "Lancement de l'installation..."
sleep 1
venv/bin/python -m pip install flask
fi
cat << 'EOF'
=================================
Vérification des certificats SSL
==================================
EOF
bash ../web_secu/ssl.sh
cat << 'EOF'
=============================
Vérification du daemin Avahi
============================
EOF
bash ../web_secu/avahi.sh
cat << 'EOF'
================================
Lancement du serveur FLASK
================================
EOF
sleep 1
touch /var/log/loustique.log
chown ${SUDO_USER}:${SUDO_USER} /var/log/loustique.log
venv/bin/python composants/test/bouton.py
venv/bin/python main.py

6
flask/.env Normal file
View File

@@ -0,0 +1,6 @@
DB_HOST=127.0.0.1
DB_PORT=3306
DB_USER=python
DB_PASSWORD=wolaouais
DB_NAME=Utilisateurs
DB_CHARSET=utf8mb4

View File

@@ -48,7 +48,32 @@ def login(username, password):
finally: finally:
cursor.close() cursor.close()
conn.close() conn.close()
def get_user_by_rfid(RFID):
conn = init()
if conn is None:
return None
try:
cursor = conn.cursor()
requete = "SELECT username FROM Auth WHERE RFID = %s"
cursor.execute(requete, (RFID))
resultat = cursor.fetchone()
if resultat:
username = resultat[0]
log.info(f"Badge RFID reconnu pour l'utilisateur : {username}")
return username
else:
log.info(f"Tentative RFID refusée : badge {RFID} inconnu.")
return None
except pymysql.err.OperationalError as e:
print(f"Erreur SQL RFID : {e}")
log.error(f"Erreur SQL RFID : {e}")
return None
finally:
if conn:
cursor.close()
conn.close()
def get_users(): def get_users():
conn = init() conn = init()

219
flask/index.html Normal file
View File

@@ -0,0 +1,219 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Loustiques Home - Connexion</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, sans-serif;
background: #1f1f1f;
color: #f0f0f0;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.carte {
width: 380px;
max-width: 95vw;
background: #2a2a2a;
border-radius: 8px;
padding: 40px 36px;
}
h1 {
font-size: 26px;
font-weight: 700;
margin-bottom: 8px;
text-align: center;
}
.soustitre {
font-size: 14px;
color: #888;
margin-bottom: 36px;
text-align: center;
}
.champ {
margin-bottom: 18px;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 6px;
color: #aaa;
}
input {
width: 100%;
padding: 11px 14px;
font-size: 14px;
font-family: inherit;
border: 1px solid #3a3a3a;
border-radius: 6px;
outline: none;
transition: border-color 0.15s;
color: #f0f0f0;
background: #333;
}
input:focus {
border-color: #2563eb;
}
input::placeholder {
color: #555;
}
button {
width: 100%;
margin-top: 10px;
padding: 12px;
background: #2563eb;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer;
transition: background 0.15s;
}
button:hover { background: #1d4ed8; }
button:active { background: #1e40af; }
button:disabled { opacity: 0.4; cursor: not-allowed; }
.message {
margin-top: 14px;
font-size: 13px;
display: none;
}
.message.error {
display: block;
color: #f87171;
}
.message.success {
display: block;
color: #4ade80;
}
.footer {
margin-top: 28px;
font-size: 12px;
color: #555;
}
</style>
</head>
<body>
<div class="carte">
<h1>Loustiques Home</h1>
<p class="soustitre">Connectez-vous via mot de passe ou avec votre bagde pour accéder à votre espace.</p>
<div class="champ">
<label for="username">Identifiant</label>
<input type="text" id="username" placeholder="Nom du loustique" autocomplete="username" />
</div>
<div class="champ">
<label for="password">Mot de passe</label>
<input type="password" id="password" placeholder="Wola ouais" autocomplete="current-password" />
</div>
<button id="btn" onclick="handleLogin()">Se connecter</button>
<div class="message" id="msg"></div>
<p class="footer">Accès réservé aux utilisateurs ajoutés par les loustiques.</p>
</div>
<script>
["username", "password"].forEach(id => {
document.getElementById(id).addEventListener("keydown", e => {
if (e.key === "Enter") handleLogin();
});
});
async function handleLogin() {
const username = document.getElementById("username").value.trim();
const password = document.getElementById("password").value;
const btn = document.getElementById("btn");
const msg = document.getElementById("msg");
msg.className = "message";
if (!username || !password) {
msg.className = "message error";
msg.textContent = "Veuillez remplir tous les champs.";
return;
}
btn.disabled = true;
btn.textContent = "Vérification...";
try {
const res = await fetch("/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password })
});
const data = await res.json();
if (data.success) {
msg.className = "message success";
msg.textContent = "Connexion réussie !";
window.location.href = "/dashboard";
} else {
msg.className = "message error";
msg.textContent = data.message || "Identifiants incorrects.";
}
} catch {
msg.className = "message error";
msg.textContent = "Impossible de contacter le serveur.";
} finally {
btn.disabled = false;
btn.textContent = "Se connecter";
}
}
setInterval(async () => {
try {
// Attention : Vérifie que cette route correspond bien à celle dans main.py
// (J'avais mis /check-rfid-login dans mon exemple précédent)
const res = await fetch('/check-rfid-login');
const data = await res.json();
if (data.success) {
const msg = document.getElementById("msg");
const btn = document.getElementById("btn");
const inputs = document.querySelectorAll("input");
btn.disabled = true;
inputs.forEach(input => input.disabled = true);
msg.className = "message success";
msg.textContent = "Badge reconnu ! Bienvenue " + data.username + "...";
setTimeout(() => {
window.location.href = "/dashboard";
}, 1000);
}
} catch (e) {
// Erreurs ignorées silencieusement
}
}, 1500);
</script>
</body>
</html>

View File

@@ -1,22 +0,0 @@
from time import *
from gpiozero import *
from signal import pause
from log import log
def led(utilisateur):
print('led allumé')
log.info(f'led allumé par {utilisateur}')
"""
print("One button to turn on or off")
led = LED(17)
btn = Button(20, bounce_time=None)
while True:
print('turn on')
led.on()
sleep(0.2) # to avoid bouncing
btn.wait_for_press()
print('turn off')
led.off()
sleep(0.2) # to avoid bouncing
btn.wait_for_press()
"""

View File

@@ -1,16 +1,32 @@
from flask import Flask, render_template, request, jsonify from flask import Flask, render_template, request, jsonify
import requests
from flask_talisman import Talisman from flask_talisman import Talisman
from led import led from led import led
import os import os
import threading
import sys
import log
from add_user import add_user from add_user import add_user
import auth import auth
import re import re
app = Flask(__name__) app = Flask(__name__)
Talisman(app, force_https=True, Talisman(app, force_https=True,
content_security_policy=False ) content_security_policy=False)
current_user = None current_user = None
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
composants = os.path.join(BASE_DIR, "composants", "byPanda")
sys.path.insert(0, composants)
from lumieres import SystemeLumieres
from board1main import call_board1
import alarme
@app.route("/") @app.route("/")
def index(): def index():
return render_template("index.html") return render_template("index.html")
@@ -32,9 +48,46 @@ def dashboard():
@app.route("/led", methods=["POST"]) @app.route("/led", methods=["POST"])
def call_led(): def call_led():
led(current_user) etat = SystemeLumieres.mettreAJourEtat()
if (etat == 0):
SystemeLumieres.allumerLumieres
else:
SystemeLumieres.eteindreLumieres()
return jsonify({"success": True})
dernier_badge_scanne = None
@app.route("/rfid-scan", methods=["POST"])
def rfid_scan():
global dernier_badge_scanne
data = request.get_json()
badge_id = data.get("badge_id")
username = auth.get_user_by_rfid(badge_id)
if username:
dernier_badge_scanne = username
return jsonify({"success": True, "username": username})
else:
return jsonify({"success": False})
@app.route("/check-rfid-login", methods=["GET"])
def check_rfid_login():
global dernier_badge_scanne
global current_user
if dernier_badge_scanne:
user = dernier_badge_scanne
current_user = user
dernier_badge_scanne = None
return jsonify({"success": True, "username": user})
return jsonify({"success": False})
"""
@app.route("/alarme",methods=["POST"])
def armer_alarme():
SystemeAlarme.armer()
return jsonify({"success": True}) return jsonify({"success": True})
"""
@app.route("/admin") @app.route("/admin")
def admin_page(): def admin_page():
return render_template("admin.html") return render_template("admin.html")
@@ -42,8 +95,6 @@ def admin_page():
@app.route("/admin/logs") @app.route("/admin/logs")
def logs_page(): def logs_page():
return render_template("log.html") return render_template("log.html")
@app.route("/admin/logs/data") @app.route("/admin/logs/data")
def get_logs(): def get_logs():
try: try:
@@ -69,11 +120,39 @@ def get_users():
users = auth.get_users() users = auth.get_users()
return jsonify({"success": True, "users": users}) return jsonify({"success": True, "users": users})
import os @app.route("/alarme_status")
def get_alarme_status_info():
try:
# On appelle la fonction corrigée dans alarme.py
statut = alarme.etat_alarme()
return jsonify({
"success": True,
"status": statut # Retournera "Désarmée", "Armée" ou "Déclenchée"
})
except Exception as e:
return jsonify({"success": False, "message": str(e)}), 500
@app.route("/api/<action>", methods=["GET"])
def relais_pi2(action):
url_pi2 = f"https://pi32.local:8000/{action}"
try:
reponse = requests.get(url_pi2, timeout=5, verify=False)
if not reponse.ok:
return jsonify({"success": False, "message": "Erreur Pi 2"}), reponse.status_code
return jsonify(reponse.json())
except Exception as e:
# On ne garde que l'erreur si vraiment ça plante
print(f"[RELAIS] ERREUR : {e}")
return jsonify({"success": False, "message": str(e)}), 500
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if __name__ == "__main__": if __name__ == "__main__":
print("[*] Démarrage du lecteur RFID et de l'alarme en arrière-plan...")
thread_hardware = threading.Thread(target=call_board1, daemon=True)
thread_hardware.start()
app.run( app.run(
host="0.0.0.0", host="0.0.0.0",
port=443, port=443,

View File

@@ -58,7 +58,11 @@ sleep 1
#---------------------------- #----------------------------
# 2. Installation de MariaDB x phpmyadmin # 2. Installation de MariaDB x phpmyadmin
#------------------------------ #------------------------------
read -p "Installer mariadb et phpMyAdmin ? (y/n)" db
if [ "$db" = 'y'];then
bash DB/main.sh bash DB/main.sh
fi
#------------------------------- #-------------------------------
@@ -98,7 +102,6 @@ VENV_LIST=()
for dir in "${SEARCH_DIRS[@]}"; do for dir in "${SEARCH_DIRS[@]}"; do
if [ -d "$dir" ]; then if [ -d "$dir" ]; then
# Un venv valide contient bin/activate et bin/python
while IFS= read -r -d '' activate_path; do while IFS= read -r -d '' activate_path; do
venv_dir=$(dirname "$(dirname "$activate_path")") venv_dir=$(dirname "$(dirname "$activate_path")")
if [ -f "$venv_dir/bin/python" ]; then if [ -f "$venv_dir/bin/python" ]; then
@@ -154,27 +157,26 @@ if [[ "$CREATE_VENV" =~ ^[oO]$ ]]; then
echo "Installation des dépendances depuis requirements.txt..." echo "Installation des dépendances depuis requirements.txt..."
"$VENV_PATH/bin/pip" install -r ./requirements.txt "$VENV_PATH/bin/pip" install -r ./requirements.txt
echo " Dépendances installées" echo " Dépendances installées"
echo " Dépendances installées"
else else
echo " Aucun requirements.txt trouvé, installation des dépendances ignorée" echo " Aucun requirements.txt trouvé, installation des dépendances ignorée"
echo " Aucun requirements.txt trouvé, installation des dépendances ignorée"
fi fi
else else
echo " Erreur lors de la création du venv à : $VENV_PATH"
echo " Erreur lors de la création du venv à : $VENV_PATH" echo " Erreur lors de la création du venv à : $VENV_PATH"
exit 1 exit 1
fi fi
else else
echo " Création ignorée" echo " Création ignorée"
echo " Création ignorée"
fi fi
# ---------------------------- # ----------------------------
# Fin # Fin
# ---------------------------- # ----------------------------
print_step " Configuration terminée" print_step " Configuration terminée"
print_step " Configuration terminée"
echo "" echo ""
if [ -f "./.venv_path" ]; then if [ -f "./.venv_path" ]; then
echo "Venv configuré : $(cat ./.venv_path)" echo "Venv configuré : $(cat ./.venv_path)"

View File

@@ -46,14 +46,14 @@ Vérification des certificats SSL
================================== ==================================
EOF EOF
bash web_secu/ssl.sh bash ../web_secu/ssl.sh
cat << 'EOF' cat << 'EOF'
============================= =============================
Vérification du daemin Avahi Vérification du daemon Avahi
============================ ============================
EOF EOF
bash web_secu/avahi.sh bash ../web_secu/avahi.sh
cat << 'EOF' cat << 'EOF'
@@ -65,4 +65,4 @@ EOF
sleep 1 sleep 1
touch /var/log/loustique.log touch /var/log/loustique.log
chown ${SUDO_USER}:${SUDO_USER} /var/log/loustique.log chown ${SUDO_USER}:${SUDO_USER} /var/log/loustique.log
venv/bin/python ./flask/main.py venv/bin/python main.py

View File

@@ -3,33 +3,14 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Admin — Loustiques</title> <title>Loustiques Home - Admin</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f0f;
--surface: #181818;
--surface2: #222222;
--border: rgba(255,255,255,0.08);
--border-hover: rgba(255,255,255,0.18);
--text: #f0ede8;
--muted: #888880;
--accent: #c8f060;
--accent-dim: rgba(200,240,96,0.12);
--danger: #ff5f57;
--danger-dim: rgba(255,95,87,0.12);
--success: #30d158;
--success-dim: rgba(48,209,88,0.1);
--mono: 'Space Mono', monospace;
--sans: 'DM Sans', sans-serif;
}
body { body {
background: var(--bg); font-family: system-ui, sans-serif;
color: var(--text); background: #1f1f1f;
font-family: var(--sans); color: #f0f0f0;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
} }
@@ -37,8 +18,8 @@
aside { aside {
width: 220px; width: 220px;
min-height: 100vh; min-height: 100vh;
background: var(--surface); background: #1a1a1a;
border-right: 1px solid var(--border); border-right: 1px solid #2e2e2e;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 2rem 1.25rem; padding: 2rem 1.25rem;
@@ -47,22 +28,20 @@
} }
.logo { .logo {
font-family: var(--mono); font-size: 20px;
font-size: 13px; font-weight: 700;
color: var(--accent); color: #f0f0f0;
letter-spacing: 0.12em;
text-transform: uppercase;
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border); border-bottom: 1px solid #2e2e2e;
} }
.logo span { .logo span {
display: block; display: block;
font-size: 10px; font-size: 15px;
color: var(--muted); color: #666;
margin-top: 4px; margin-top: 4px;
letter-spacing: 0.06em; font-weight: 400;
} }
nav a { nav a {
@@ -71,27 +50,26 @@
gap: 10px; gap: 10px;
padding: 8px 10px; padding: 8px 10px;
border-radius: 6px; border-radius: 6px;
font-size: 13.5px; font-size: 13px;
color: var(--muted); color: #888;
text-decoration: none; text-decoration: none;
margin-bottom: 2px; margin-bottom: 2px;
transition: all 0.15s; transition: all 0.15s;
} }
nav a.active, nav a:hover { nav a.active, nav a:hover {
background: var(--accent-dim); background: #2e2e3a;
color: var(--accent); color: #2563eb;
} }
nav a svg { width: 15px; height: 15px; flex-shrink: 0; } nav a svg { width: 15px; height: 15px; flex-shrink: 0; }
.sidebar-footer { .sidebar-footer {
margin-top: auto; margin-top: auto;
font-size: 12px; font-size: 15px;
color: var(--muted); color: #555;
font-family: var(--mono);
padding-top: 1rem; padding-top: 1rem;
border-top: 1px solid var(--border); border-top: 1px solid #2e2e2e;
} }
main { main {
@@ -104,34 +82,29 @@
.page-header { .page-header {
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
} }
.page-header h1 { .page-header h1 {
font-family: var(--mono);
font-size: 22px; font-size: 22px;
font-weight: 700; font-weight: 700;
color: var(--text);
margin-bottom: 4px; margin-bottom: 4px;
} }
.page-header p { .page-header p {
font-size: 14px; font-size: 14px;
color: var(--muted); color: #888;
} }
.card { .card {
background: var(--surface); background: #2a2a2a;
border: 1px solid var(--border); border: 1px solid #333;
border-radius: 12px; border-radius: 8px;
padding: 1.75rem; padding: 1.75rem;
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
} }
.card-title { .card-title {
font-family: var(--mono); font-size: 15px;
font-size: 11px;
letter-spacing: 0.1em; letter-spacing: 0.1em;
text-transform: uppercase; text-transform: uppercase;
color: var(--muted); color: #666;
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
} }
@@ -146,177 +119,125 @@
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
.form-group.full {
.form-group.full { grid-column: 1 / -1; } grid-column: 1 / -1;
}
label { label {
font-size: 12px; font-size: 12px;
color: var(--muted); color: #888;
font-family: var(--mono);
letter-spacing: 0.05em;
} }
input[type="text"], input[type="text"],
input[type="password"] { input[type="password"],
background: var(--surface2); select {
border: 1px solid var(--border); background: #333;
border-radius: 7px; border: 1px solid #3a3a3a;
border-radius: 6px;
padding: 10px 14px; padding: 10px 14px;
color: var(--text); color: #f0f0f0;
font-family: var(--mono); font-family: inherit;
font-size: 13px; font-size: 13px;
outline: none; outline: none;
transition: border-color 0.15s;
width: 100%; width: 100%;
transition: border-color 0.15s;
appearance: none;
} }
input:focus { input:focus, select:focus { border-color: #2563eb; }
border-color: var(--accent); input::placeholder { color: #555; }
}
.btn { .btn {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 10px 20px; padding: 9px 18px;
border-radius: 7px; border-radius: 6px;
border: none; border: none;
font-family: var(--mono); font-family: inherit;
font-size: 12px; font-size: 13px;
letter-spacing: 0.05em;
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
} }
.btn-primary { .btn-primary { background: #2563eb; color: #fff; font-weight: 600; }
background: var(--accent); .btn-primary:hover { background: #1d4ed8; }
color: #0f0f0f;
font-weight: 700;
}
.btn-primary:hover { background: #d4f570; }
.btn-danger { .btn-danger {
background: var(--danger-dim); background: transparent;
color: var(--danger); color: #f87171;
border: 1px solid rgba(255,95,87,0.2); border: 1px solid rgba(248,113,113,0.3);
font-size: 12px;
padding: 6px 14px;
} }
.btn-danger:hover { background: rgba(248,113,113,0.1); }
.btn-danger:hover { background: rgba(255,95,87,0.2); } .form-actions { display: flex; justify-content: flex-end; margin-top: 1.25rem; }
.form-actions { .user-list { display: flex; flex-direction: column; }
display: flex;
justify-content: flex-end;
margin-top: 1.25rem;
}
.user-list { display: flex; flex-direction: column; gap: 0; }
.user-row { .user-row {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 12px 0; padding: 12px 0;
border-bottom: 1px solid var(--border); border-bottom: 1px solid #333;
gap: 14px; gap: 14px;
transition: background 0.1s;
} }
.user-row:last-child { border-bottom: none; } .user-row:last-child { border-bottom: none; }
.avatar { .avatar {
width: 36px; width: 36px; height: 36px;
height: 36px;
border-radius: 50%; border-radius: 50%;
background: var(--accent-dim); background: #2e2e3a;
border: 1px solid rgba(200,240,96,0.2); border: 1px solid #3a3a4a;
display: flex; display: flex; align-items: center; justify-content: center;
align-items: center; font-size: 11px; font-weight: 600;
justify-content: center; color: #2563eb;
font-family: var(--mono);
font-size: 11px;
color: var(--accent);
flex-shrink: 0; flex-shrink: 0;
} }
.user-info { flex: 1; } .user-info { flex: 1; }
.user-name { font-size: 14px; font-weight: 500; margin-bottom: 2px; }
.user-name { .user-meta { font-size: 11px; color: #666; }
font-size: 14px;
font-weight: 500;
color: var(--text);
margin-bottom: 2px;
}
.user-meta {
font-size: 11px;
color: var(--muted);
font-family: var(--mono);
}
.badge { .badge {
font-size: 10px; font-size: 10px;
font-family: var(--mono);
padding: 3px 8px; padding: 3px 8px;
border-radius: 4px; border-radius: 4px;
letter-spacing: 0.05em;
} }
.badge-admin { .badge-admin { background: rgba(37,99,235,0.15); color: #2563eb; border: 1px solid rgba(37,99,235,0.3); }
background: var(--accent-dim); .badge-user { background: #333; color: #888; border: 1px solid #3a3a3a; }
color: var(--accent);
border: 1px solid rgba(200,240,96,0.2);
}
.badge-user {
background: var(--surface2);
color: var(--muted);
border: 1px solid var(--border);
}
.toast { .toast {
position: fixed; position: fixed;
bottom: 2rem; bottom: 2rem; right: 2rem;
right: 2rem; background: #2a2a2a;
background: var(--surface); border: 1px solid #333;
border: 1px solid var(--border);
border-radius: 8px; border-radius: 8px;
padding: 12px 18px; padding: 12px 18px;
font-size: 13px; font-size: 13px;
font-family: var(--mono); color: #f0f0f0;
color: var(--text);
transform: translateY(20px); transform: translateY(20px);
opacity: 0; opacity: 0;
transition: all 0.25s; transition: all 0.25s;
pointer-events: none; pointer-events: none;
z-index: 999; z-index: 999;
} }
.toast.show { transform: translateY(0); opacity: 1; } .toast.show { transform: translateY(0); opacity: 1; }
.toast.success { border-color: rgba(48,209,88,0.3); color: var(--success); } .toast.success { border-color: rgba(74,222,128,0.3); color: #4ade80; }
.toast.error { border-color: rgba(255,95,87,0.3); color: var(--danger); } .toast.error { border-color: rgba(248,113,113,0.3); color: #f87171; }
.strength-bar { .strength-bar { height: 3px; background: #333; border-radius: 2px; margin-top: 6px; overflow: hidden; }
height: 3px; .strength-fill { height: 100%; border-radius: 2px; transition: width 0.3s, background 0.3s; width: 0%; }
background: var(--surface2);
border-radius: 2px;
margin-top: 6px;
overflow: hidden;
}
.strength-fill {
height: 100%;
border-radius: 2px;
transition: width 0.3s, background 0.3s;
width: 0%;
}
</style> </style>
</head> </head>
<body> <body>
<aside> <aside>
<div class="logo"> <div class="logo">
Loustiques SUPER Loustiques
<span>panneau admin</span> <span>Panneau admin</span>
</div> </div>
<nav> <nav>
<a href="#" class="active"> <a href="#" class="active">
@@ -332,14 +253,12 @@
Système Système
</a> </a>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">Version 1.0</div>
v0.1.0 — local
</div>
</aside> </aside>
<main> <main>
<div class="page-header"> <div class="page-header">
<h1>utilisateurs</h1> <h1>Utilisateurs</h1>
<p>Créer et gérer les comptes d'accès.</p> <p>Créer et gérer les comptes d'accès.</p>
</div> </div>
@@ -347,23 +266,23 @@
<div class="card-title">Créer un utilisateur</div> <div class="card-title">Créer un utilisateur</div>
<div class="form-grid"> <div class="form-grid">
<div class="form-group"> <div class="form-group">
<label>nom d'utilisateur</label> <label>Nom d'utilisateur</label>
<input type="text" id="new-username" placeholder="ex: maxime" autocomplete="off"> <input type="text" id="new-username" placeholder="ex: maxime" autocomplete="off">
</div> </div>
<div class="form-group"> <div class="form-group">
<label>rôle</label> <label>Rôle</label>
<select id="new-role" style="background:var(--surface2);border:1px solid var(--border);border-radius:7px;padding:10px 14px;color:var(--text);font-family:var(--mono);font-size:13px;outline:none;width:100%;appearance:none;"> <select id="new-role">
<option value="user">user</option> <option value="user">user</option>
<option value="admin">admin</option> <option value="admin">admin</option>
</select> </select>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>mot de passe</label> <label>Mot de passe</label>
<input type="password" id="new-password" placeholder="••••••••" oninput="checkStrength(this.value)"> <input type="password" id="new-password" placeholder="••••••••" oninput="checkStrength(this.value)">
<div class="strength-bar"><div class="strength-fill" id="strength-fill"></div></div> <div class="strength-bar"><div class="strength-fill" id="strength-fill"></div></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label>confirmer</label> <label>Confirmer</label>
<input type="password" id="confirm-password" placeholder="••••••••"> <input type="password" id="confirm-password" placeholder="••••••••">
</div> </div>
</div> </div>
@@ -377,17 +296,7 @@
<div class="card"> <div class="card">
<div class="card-title">Comptes existants</div> <div class="card-title">Comptes existants</div>
<div class="user-list" id="user-list"> <div class="user-list" id="user-list"></div>
<div class="user-row">
<div class="avatar">MA</div>
<div class="user-info">
<div class="user-name">maxime</div>
<div class="user-meta">créé le 24/03/2026</div>
</div>
<span class="badge badge-admin">admin</span>
<button class="btn btn-danger" onclick="deleteUser(this, 'maxime')">Supprimer</button>
</div>
</div>
</div> </div>
</main> </main>
@@ -408,7 +317,7 @@ function checkStrength(val) {
if (/[A-Z]/.test(val)) score++; if (/[A-Z]/.test(val)) score++;
if (/[0-9]/.test(val)) score++; if (/[0-9]/.test(val)) score++;
if (/[^A-Za-z0-9]/.test(val)) score++; if (/[^A-Za-z0-9]/.test(val)) score++;
const colors = ['#ff5f57', '#ff9f0a', '#ffd60a', '#30d158']; const colors = ['#f87171', '#fb923c', '#facc15', '#4ade80'];
fill.style.width = (score * 25) + '%'; fill.style.width = (score * 25) + '%';
fill.style.background = colors[score - 1] || 'transparent'; fill.style.background = colors[score - 1] || 'transparent';
} }
@@ -443,6 +352,7 @@ function createUser() {
}) })
.catch(() => showToast('Erreur réseau', 'error')); .catch(() => showToast('Erreur réseau', 'error'));
} }
function loadUsers() { function loadUsers() {
fetch('/admin/get_users') fetch('/admin/get_users')
.then(r => r.json()) .then(r => r.json())
@@ -455,6 +365,7 @@ function loadUsers() {
} }
loadUsers(); loadUsers();
function addUserRow(username, role) { function addUserRow(username, role) {
const initials = username.slice(0, 2).toUpperCase(); const initials = username.slice(0, 2).toUpperCase();
const today = new Date().toLocaleDateString('fr-FR'); const today = new Date().toLocaleDateString('fr-FR');

View File

@@ -3,208 +3,47 @@
<head> <head>
<meta charset="UTF-8"/> <meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Loustique Home — Dashboard</title> <title>Loustiques Home — Dashboard</title>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet"/>
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #1f1f1f; color: #f0f0f0; min-height: 100vh; }
:root { header { display: flex; align-items: center; justify-content: space-between; padding: 18px 32px; border-bottom: 1px solid #2e2e2e; background: #1a1a1a; }
--bg: #0a0a0f; .logo { font-size: 17px; font-weight: 700; }
--panel: #111118;
--border: #1e1e2e;
--accent: #7c6aff;
--accent2: #ff6ab0;
--text: #e8e6f0;
--muted: #6b6880;
--success: #4ade80;
--warning: #fbbf24;
}
body {
font-family: 'DM Mono', monospace;
background: var(--bg);
color: var(--text);
min-height: 100vh;
overflow-x: hidden;
}
/* Grid background */
body::before {
content: '';
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(124,106,255,0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(124,106,255,0.03) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
z-index: 0;
}
.orb { position: fixed; border-radius: 50%; filter: blur(100px); opacity: 0.08; pointer-events: none; z-index: 0; }
.orb1 { width: 600px; height: 600px; background: var(--accent); top: -200px; left: -200px; }
.orb2 { width: 400px; height: 400px; background: var(--accent2); bottom: -100px; right: -100px; }
/* ── TOPBAR ── */
header {
position: relative; z-index: 10;
display: flex; align-items: center; justify-content: space-between;
padding: 20px 36px;
border-bottom: 1px solid var(--border);
background: rgba(10,10,15,0.8);
backdrop-filter: blur(12px);
animation: fadeDown 0.5s ease forwards;
}
@keyframes fadeDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
.logo {
font-family: 'Syne', sans-serif;
font-size: 18px; font-weight: 800;
letter-spacing: -0.02em;
}
.logo span { background: linear-gradient(135deg, var(--accent), var(--accent2)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.header-right { display: flex; align-items: center; gap: 20px; } .header-right { display: flex; align-items: center; gap: 20px; }
.status-dot { display: flex; align-items: center; gap: 8px; font-size: 12px; color: #888; }
.status-dot { .dot { width: 7px; height: 7px; border-radius: 50%; background: #4ade80; animation: pulse 2s infinite; }
display: flex; align-items: center; gap: 8px;
font-size: 11px; color: var(--muted); letter-spacing: 0.05em;
}
.dot { width: 7px; height: 7px; border-radius: 50%; background: var(--success); box-shadow: 0 0 6px var(--success); animation: pulse 2s infinite; }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } } @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
.logout { font-family: inherit; font-size: 12px; color: #888; background: none; border: 1px solid #3a3a3a; padding: 6px 14px; border-radius: 6px; cursor: pointer; transition: color 0.15s, border-color 0.15s; }
.logout { .logout:hover { color: #f0f0f0; border-color: #666; }
font-family: 'DM Mono', monospace; main { max-width: 1100px; margin: 0 auto; padding: 36px 32px; }
font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; .welcome { margin-bottom: 36px; text-align: center; }
color: var(--muted); background: none; border: 1px solid var(--border); .welcome .label { font-size: 14px; letter-spacing: 0.15em; text-transform: uppercase; color: #2563eb; margin-bottom: 8px; }
padding: 6px 14px; border-radius: 2px; cursor: pointer; .welcome h1 { font-size: 30px; font-weight: 700; }
transition: color 0.2s, border-color 0.2s; .grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 32px; }
} .card { background: #2a2a2a; border: 1px solid #333; border-radius: 8px; padding: 22px; position: relative; }
.logout:hover { color: var(--text); border-color: var(--muted); } .card-label { font-size: 11px; letter-spacing: 0.1em; text-transform: uppercase; color: #888; margin-bottom: 10px; }
.card-value { font-size: 22px; font-weight: 700; }
/* ── MAIN ── */ .card-sub { font-size: 12px; color: #666; margin-top: 6px; }
main { .card-icon { position: absolute; top: 20px; right: 20px; width: 28px; height: 28px; opacity: 0.15; }
position: relative; z-index: 1; .section-title { font-size: 11px; letter-spacing: 0.15em; text-transform: uppercase; color: #666; margin-bottom: 14px; display: flex; align-items: center; gap: 10px; }
max-width: 1100px; margin: 0 auto; .section-title::after { content: ''; flex: 1; height: 1px; background: #2e2e2e; }
padding: 40px 36px; .actions { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
} .action-btn { background: #2a2a2a; border: 1px solid #333; border-radius: 8px; padding: 18px 20px; cursor: pointer; text-align: left; color: #f0f0f0; position: relative; transition: border-color 0.15s, background 0.15s; }
.action-btn:hover { border-color: #2563eb; background: #2e2e3a; }
.welcome {
margin-bottom: 40px;
animation: fadeUp 0.5s 0.1s ease both;
}
@keyframes fadeUp { from { opacity: 0; transform: translateY(16px); } to { opacity: 1; transform: translateY(0); } }
.welcome .label { font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--accent); margin-bottom: 8px; }
.welcome h1 { font-family: 'Syne', sans-serif; font-size: 28px; font-weight: 800; letter-spacing: -0.02em; }
.welcome h1 span { background: linear-gradient(135deg, var(--accent), var(--accent2)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
/* ── GRID ── */
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.card {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 2px;
padding: 24px;
position: relative;
overflow: hidden;
animation: fadeUp 0.5s ease both;
}
.card:nth-child(1) { animation-delay: 0.15s; }
.card:nth-child(2) { animation-delay: 0.22s; }
.card:nth-child(3) { animation-delay: 0.29s; }
.card:nth-child(4) { animation-delay: 0.36s; }
.card::before {
content: '';
position: absolute; top: 0; left: 0; right: 0;
height: 1px;
background: linear-gradient(90deg, var(--accent), transparent);
}
.card-label { font-size: 10px; letter-spacing: 0.15em; text-transform: uppercase; color: var(--muted); margin-bottom: 12px; }
.card-value { font-family: 'Syne', sans-serif; font-size: 28px; font-weight: 800; letter-spacing: -0.02em; }
.card-sub { font-size: 11px; color: var(--muted); margin-top: 6px; }
.card-icon {
position: absolute; top: 20px; right: 20px;
width: 32px; height: 32px; opacity: 0.15;
}
/* ── ACTIONS ── */
.section-title {
font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase;
color: var(--muted); margin-bottom: 16px;
display: flex; align-items: center; gap: 10px;
animation: fadeUp 0.5s 0.4s ease both;
}
.section-title::after { content: ''; flex: 1; height: 1px; background: var(--border); }
.actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
animation: fadeUp 0.5s 0.45s ease both;
}
.action-btn {
background: var(--panel);
border: 1px solid var(--border);
border-radius: 2px;
padding: 18px 20px;
cursor: pointer;
text-align: left;
transition: border-color 0.2s, background 0.2s, transform 0.1s;
position: relative; overflow: hidden;
color: var(--text); /* ← ajouter ça */
}
.action-btn:hover { border-color: var(--accent); background: rgba(124,106,255,0.05); }
.action-btn:active { transform: scale(0.98); } .action-btn:active { transform: scale(0.98); }
.action-btn .a-label { font-size: 14px; font-weight: 600; display: block; margin-bottom: 4px; }
.action-btn .a-label { .action-btn .a-sub { font-size: 12px; color: #ffffff; }
font-family: 'Syne', sans-serif; .action-btn .a-arrow { position: absolute; right: 16px; top: 50%; transform: translateY(-50%); color: #555; font-size: 18px; transition: color 0.15s, right 0.15s; }
font-size: 13px; font-weight: 700; .action-btn:hover .a-arrow { color: #2563eb; right: 12px; }
letter-spacing: 0.02em; display: block; margin-bottom: 4px; .toast { position: fixed; bottom: 24px; right: 24px; background: #2a2a2a; border: 1px solid #333; border-radius: 8px; padding: 12px 18px; font-size: 13px; color: #f0f0f0; display: flex; align-items: center; gap: 10px; transform: translateY(20px); opacity: 0; transition: transform 0.3s, opacity 0.3s; z-index: 100; }
}
.action-btn .a-sub { font-size: 10px; color: var(--muted); letter-spacing: 0.05em; }
.action-btn .a-arrow {
position: absolute; right: 16px; top: 50%; transform: translateY(-50%);
color: var(--muted); font-size: 16px;
transition: color 0.2s, right 0.2s;
}
.action-btn:hover .a-arrow { color: var(--accent); right: 12px; }
/* ── TOAST ── */
.toast {
position: fixed; bottom: 28px; right: 28px;
background: var(--panel); border: 1px solid var(--border);
border-radius: 2px; padding: 12px 18px;
font-size: 12px; color: var(--text);
display: flex; align-items: center; gap: 10px;
transform: translateY(20px); opacity: 0;
transition: transform 0.3s, opacity 0.3s;
z-index: 100;
}
.toast.show { transform: translateY(0); opacity: 1; } .toast.show { transform: translateY(0); opacity: 1; }
.toast-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--success); flex-shrink: 0; } .toast-dot { width: 6px; height: 6px; border-radius: 50%; background: #4ade80; flex-shrink: 0; }
</style> </style>
</head> </head>
<body> <body>
<div class="orb orb1"></div>
<div class="orb orb2"></div>
<header> <header>
<div class="logo">Loustique <span>Home</span></div> <div class="logo">Loustiques Home</div>
<div class="header-right"> <div class="header-right">
<div class="status-dot"><div class="dot"></div> Système actif</div> <div class="status-dot"><div class="dot"></div> Système actif</div>
<button class="logout" onclick="window.location.href='/'">Déconnexion</button> <button class="logout" onclick="window.location.href='/'">Déconnexion</button>
@@ -214,57 +53,78 @@
<main> <main>
<div class="welcome"> <div class="welcome">
<div class="label">Tableau de bord</div> <div class="label">Tableau de bord</div>
<h1>Bienvenue <span>chez vous.</span></h1> <h1>Bienvenue chez vous</h1>
</div> </div>
<div class="grid"> <div class="grid">
<div class="card">
<svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
<div class="card-label">Statut</div>
<div class="card-value" style="color: var(--success); font-size:20px;">En ligne</div>
<div class="card-sub">Serveur opérationnel</div>
</div>
<div class="card"> <div class="card">
<svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> <svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<div class="card-label">Heure locale</div> <div class="card-label">Heure locale</div>
<div class="card-value" id="clock" style="font-size:22px;">--:--</div> <div class="card-value" id="clock">--:--</div>
<div class="card-sub" id="date-display">--</div> <div class="card-sub" id="date-display">--</div>
</div> </div>
<div class="card"> <div class="card">
<svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg> <svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/></svg>
<div class="card-label">Raspberry Pi</div> <div class="card-label">Température</div>
<div class="card-value" style="font-size:20px; color: var(--accent);">Actif</div> <div class="card-value" id="temp-display">-- °C</div>
<div class="card-sub">Flask 3.1</div> <div class="card-sub">Capteur DHT11 (Pi 2)</div>
</div> </div>
<div class="card"> <div class="card">
<svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> <svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<div class="card-label">Session</div> <path d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707m12.728 0l-.707-.707M6.343 6.343l-.707-.707M12 8a4 4 0 100 8 4 4 0 000-8z"/>
<div class="card-value" style="font-size:20px;">Authentifiée</div> </svg>
<div class="card-sub">Accès autorisé</div> <div class="card-label">Luminosité</div>
<div class="card-value" id="ldr-display">--</div>
<div class="card-sub">Capteur LDR (Pi 2)</div>
</div>
<div class="card">
<svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect><line x1="9" y1="3" x2="9" y2="21"></line></svg>
<div class="card-label">Porte / Volet</div>
<div class="card-value" id="door-display">--</div>
<div class="card-sub">État du moteur (Pi 2)</div>
</div>
<div class="card">
<svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
<div class="card-label">Alarme</div>
<div class="card-value" id="alarm-display">--</div>
<div class="card-sub">Sécurité (Pi 1)</div>
</div>
<div class="card">
<svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
<div class="card-label">Raspberry Pi 1</div>
<div class="card-value" id="status-pi44">Vérification...</div>
<div class="card-sub">Serveur Flask</div>
</div>
<div class="card">
<svg class="card-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="3" width="20" height="14" rx="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/></svg>
<div class="card-label">Raspberry Pi 2</div>
<div class="card-value" id="status-pi32">Vérification...</div>
<div class="card-sub">Serveur FastAPI</div>
</div> </div>
</div> </div>
<div class="section-title">Actions rapides</div> <div class="section-title">Actions rapides</div>
<div class="actions"> <div class="actions">
<button class="action-btn" onclick="callLed()"> <button class="action-btn" onclick="call_led_up()">
<span class="a-label">💡 LED</span> <span class="a-label">💡 UP LED</span>
<span class="a-sub">Contrôler la LED</span> <span class="a-sub">Allumer la LED (Pi 2)</span>
<span class="a-arrow"></span> <span class="a-arrow"></span>
</button> </button>
<button class="action-btn" onclick="showToast('Fonction à venir...')">
<span class="a-label">⚙️ Paramètres</span> <button class="action-btn" onclick="call_led_down()">
<span class="a-sub">Configuration système</span> <span class="a-label">💡 DOWN LED</span>
<span class="a-sub">Éteindre la LED (Pi 2)</span>
<span class="a-arrow"></span> <span class="a-arrow"></span>
</button> </button>
<button class="action-btn" onclick="showToast('Fonction à venir...')">
<span class="a-label">📡 Réseau</span> <button class="action-btn" onclick="go_admin()">
<span class="a-sub">État de la connexion</span>
<span class="a-arrow"></span>
</button>
<button class="action-btn" onclick="go_admin('Fonction à venir...')">
<span class="a-label">Administration</span> <span class="a-label">Administration</span>
<span class="a-sub">Administrer les loustiques</span> <span class="a-sub">Gérer les utilisateurs</span>
<span class="a-arrow"></span> <span class="a-arrow"></span>
</button> </button>
</div> </div>
@@ -276,34 +136,91 @@
</div> </div>
<script> <script>
function updateClock() { function updateClock() {
const now = new Date(); const now = new Date();
document.getElementById("clock").textContent = document.getElementById("clock").textContent = now.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" });
now.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" }); document.getElementById("date-display").textContent = now.toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long" });
document.getElementById("date-display").textContent =
now.toLocaleDateString("fr-FR", { weekday: "long", day: "numeric", month: "long" });
} }
updateClock();
setInterval(updateClock, 1000); setInterval(updateClock, 1000);
updateClock();
async function updateAll() {
console.log("Rafraîchissement des données...");
async function callLed() {
try { try {
const res = await fetch('/led', { const resA = await fetch('/alarme_status');
method: 'POST', const dataA = await resA.json();
headers: { 'Content-Type': 'application/json' } const elA = document.getElementById("alarm-display");
}); const elS1 = document.getElementById("status-pi44");
const text = await res.text();
showToast("LED activée !"); if (dataA.success) {
} catch { elA.textContent = dataA.status;
showToast("Erreur lors de l'appel LED."); elA.style.color = (dataA.status === "desarmee") ? "#4ade80" : "#f87171";
elS1.textContent = "Actif";
elS1.style.color = "#4ade80";
}
} catch (e) {
document.getElementById("status-pi44").textContent = "Hors ligne";
document.getElementById("status-pi44").style.color = "#f87171";
}
try {
const resL = await fetch('/api/luminosite');
const dataL = await resL.json();
const elL = document.getElementById("ldr-display");
if (dataL.success) {
elL.textContent = dataL.status;
elL.style.color = (dataL.status === "Jour") ? "#fbbf24" : "#818cf8";
}
} catch (e) {
console.log("Erreur de lecture luminosité");
document.getElementById("ldr-display").textContent = "Erreur";
}
try {
const resV = await fetch('/api/volet_status');
const dataV = await resV.json();
const elV = document.getElementById("door-display");
const elS2 = document.getElementById("status-pi32");
if (dataV.success) {
elV.textContent = dataV.status;
elV.style.color = (dataV.status === "Ouvert") ? "#4ade80" : "#f87171";
elS2.textContent = "Actif";
elS2.style.color = "#4ade80";
} else {
elS2.textContent = "Injoignable";
elS2.style.color = "#fbbf24";
}
} catch (e) {
document.getElementById("status-pi32").textContent = "Hors ligne";
document.getElementById("status-pi32").style.color = "#f87171";
}
try {
const resT = await fetch('/api/temperature');
const dataT = await resT.json();
if (dataT.success) {
document.getElementById("temp-display").textContent = dataT.temperature + " °C";
}
} catch (e) {
document.getElementById("temp-display").textContent = "-- °C";
} }
} }
setInterval(updateAll, 15000);
function go_admin (){ updateAll();
window.location.href = "/admin"; async function call_led_up() {
try {
await fetch('/api/up_led');
showToast("LED allumée !");
} catch { showToast("Erreur Pi 2"); }
} }
async function call_led_down() {
try {
await fetch('/api/down_led');
showToast("LED éteinte !");
} catch { showToast("Erreur Pi 2"); }
}
function go_admin() { window.location.href = "/admin"; }
function showToast(msg) { function showToast(msg) {
const toast = document.getElementById("toast"); const toast = document.getElementById("toast");
document.getElementById("toast-text").textContent = msg; document.getElementById("toast-text").textContent = msg;

View File

@@ -3,180 +3,143 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/> <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Connexion</title> <title>Loustiques Home - Connexion</title>
<link href="https://fonts.googleapis.com/css2?family=Syne:wght@400;700;800&family=DM+Mono:wght@300;400&display=swap" rel="stylesheet"/>
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } * {
box-sizing: border-box;
:root { margin: 0;
--bg: #0a0a0f; padding: 0;
--panel: #111118;
--border: #1e1e2e;
--accent: #7c6aff;
--accent2: #ff6ab0;
--text: #e8e6f0;
--muted: #6b6880;
--success: #4ade80;
--error: #f87171;
} }
body { body {
font-family: 'DM Mono', monospace; font-family: system-ui, sans-serif;
background: var(--bg); background: #1f1f1f;
color: var(--text); color: #f0f0f0;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
overflow: hidden;
} }
body::before { .carte {
content: ''; width: 380px;
position: fixed;
inset: 0;
background-image:
linear-gradient(rgba(124,106,255,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(124,106,255,0.04) 1px, transparent 1px);
background-size: 40px 40px;
animation: gridShift 20s linear infinite;
pointer-events: none;
}
@keyframes gridShift {
0% { transform: translate(0, 0); }
100% { transform: translate(40px, 40px); }
}
.orb { position: fixed; border-radius: 50%; filter: blur(80px); opacity: 0.15; pointer-events: none; }
.orb1 { width: 500px; height: 500px; background: var(--accent); top: -150px; left: -150px; animation: float1 12s ease-in-out infinite; }
.orb2 { width: 400px; height: 400px; background: var(--accent2); bottom: -100px; right: -100px; animation: float2 15s ease-in-out infinite; }
@keyframes float1 { 0%, 100% { transform: translate(0,0); } 50% { transform: translate(40px,30px); } }
@keyframes float2 { 0%, 100% { transform: translate(0,0); } 50% { transform: translate(-30px,-40px); } }
.card {
position: relative;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 2px;
padding: 48px 44px;
width: 420px;
max-width: 95vw; max-width: 95vw;
animation: cardIn 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards; background: #2a2a2a;
opacity: 0; border-radius: 8px;
transform: translateY(20px); padding: 40px 36px;
} }
@keyframes cardIn { to { opacity: 1; transform: translateY(0); } } h1 {
font-size: 26px;
.card::before { font-weight: 700;
content: ''; margin-bottom: 8px;
position: absolute; text-align: center;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, var(--accent), var(--accent2));
} }
.tag { font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase; color: var(--accent); margin-bottom: 20px; display: flex; align-items: center; gap: 8px; } .soustitre {
.tag::before { content: ''; display: inline-block; width: 18px; height: 1px; background: var(--accent); } font-size: 14px;
color: #888;
margin-bottom: 36px;
text-align: center;
}
h1 { font-family: 'Syne', sans-serif; font-size: 32px; font-weight: 800; letter-spacing: -0.03em; margin-bottom: 8px; } .champ {
h1 span { background: linear-gradient(135deg, var(--accent), var(--accent2)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } margin-bottom: 18px;
}
.subtitle { font-size: 12px; color: var(--muted); margin-bottom: 36px; letter-spacing: 0.02em; } label {
display: block;
.field { margin-bottom: 20px; } font-size: 13px;
font-weight: 500;
label { display: block; font-size: 10px; letter-spacing: 0.15em; text-transform: uppercase; color: var(--muted); margin-bottom: 8px; } margin-bottom: 6px;
color: #aaa;
}
input { input {
width: 100%; width: 100%;
background: rgba(255,255,255,0.03); padding: 11px 14px;
border: 1px solid var(--border); font-size: 14px;
border-radius: 2px; font-family: inherit;
padding: 12px 14px; border: 1px solid #3a3a3a;
font-family: 'DM Mono', monospace; border-radius: 6px;
font-size: 13px;
color: var(--text);
outline: none; outline: none;
transition: border-color 0.2s, background 0.2s, box-shadow 0.2s; transition: border-color 0.15s;
color: #f0f0f0;
background: #333;
}
input:focus {
border-color: #2563eb;
}
input::placeholder {
color: #555;
} }
input:focus { border-color: var(--accent); background: rgba(124,106,255,0.05); box-shadow: 0 0 0 3px rgba(124,106,255,0.1); }
input::placeholder { color: var(--muted); }
button { button {
width: 100%; width: 100%;
margin-top: 28px; margin-top: 10px;
padding: 14px; padding: 12px;
background: var(--accent); background: #2563eb;
border: none;
border-radius: 2px;
font-family: 'Syne', sans-serif;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #fff; color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
font-family: inherit;
cursor: pointer; cursor: pointer;
position: relative; transition: background 0.15s;
overflow: hidden;
transition: background 0.2s, transform 0.1s;
} }
button:hover { background: #6a58ee; }
button:active { transform: scale(0.98); } button:hover { background: #1d4ed8; }
button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } button:active { background: #1e40af; }
button.loading::after { button:disabled { opacity: 0.4; cursor: not-allowed; }
content: '';
position: absolute; inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.15), transparent);
animation: shimmer 1.2s infinite;
}
@keyframes shimmer { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
.message { .message {
margin-top: 16px; padding: 10px 14px; border-radius: 2px; margin-top: 14px;
font-size: 12px; letter-spacing: 0.03em; font-size: 13px;
display: none; align-items: center; gap: 8px; display: none;
}
.message.error {
display: block;
color: #f87171;
}
.message.success {
display: block;
color: #4ade80;
} }
.message.success { display: flex; background: rgba(74,222,128,0.08); border: 1px solid rgba(74,222,128,0.2); color: var(--success); }
.message.error { display: flex; background: rgba(248,113,113,0.08); border: 1px solid rgba(248,113,113,0.2); color: var(--error); }
.footer { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--border); font-size: 10px; color: var(--muted); text-align: center; letter-spacing: 0.05em; } .footer {
margin-top: 28px;
font-size: 12px;
color: #555;
}
</style> </style>
</head> </head>
<body> <body>
<div class="orb orb1"></div>
<div class="orb orb2"></div>
<div class="card"> <div class="carte">
<div class="tag">Authentification</div> <h1>Loustiques Home</h1>
<h1>Loustique Home</span></h1> <p class="soustitre">Connectez-vous via mot de passe ou avec votre bagde pour accéder à votre espace.</p>
<p class="subtitle">Connectez-vous pour accéder à votre espace.</p>
<div class="field"> <div class="champ">
<label for="username">Identifiant</label> <label for="username">Identifiant</label>
<input type="text" id="username" placeholder="Nom du loustique" autocomplete="username" /> <input type="text" id="username" placeholder="Nom du loustique" autocomplete="username" />
</div> </div>
<div class="field"> <div class="champ">
<label for="password">Mot de passe</label> <label for="password">Mot de passe</label>
<input type="password" id="password" placeholder="Le secret du loustique" autocomplete="current-password" /> <input type="password" id="password" placeholder="Wola ouais" autocomplete="current-password" />
</div> </div>
<button id="btn" onclick="handleLogin()">Se connecter</button> <button id="btn" onclick="handleLogin()">Se connecter</button>
<div class="message" id="msg"> <div class="message" id="msg"></div>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path id="icon-path"/>
</svg>
<span id="msg-text"></span>
</div>
<div class="footer">Accès réservé aux Utilisateurs étant ajoutés par les loustiques</div> <p class="footer">Accès réservé aux utilisateurs ajoutés par les loustiques.</p>
</div> </div>
<script> <script>
["username", "password"].forEach(id => { ["username", "password"].forEach(id => {
document.getElementById(id).addEventListener("keydown", e => { document.getElementById(id).addEventListener("keydown", e => {
if (e.key === "Enter") handleLogin(); if (e.key === "Enter") handleLogin();
@@ -186,21 +149,22 @@
async function handleLogin() { async function handleLogin() {
const username = document.getElementById("username").value.trim(); const username = document.getElementById("username").value.trim();
const password = document.getElementById("password").value; const password = document.getElementById("password").value;
const btn = document.getElementById("btn"); const btn = document.getElementById("btn");
const msg = document.getElementById("msg");
document.getElementById("msg").className = "message"; msg.className = "message";
if (!username || !password) { if (!username || !password) {
showMessage("error", "Veuillez remplir tous les champs."); msg.className = "message error";
msg.textContent = "Veuillez remplir tous les champs.";
return; return;
} }
btn.disabled = true; btn.disabled = true;
btn.classList.add("loading");
btn.textContent = "Vérification..."; btn.textContent = "Vérification...";
try { try {
const res = await fetch("/login", { const res = await fetch("/login", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ username, password }) body: JSON.stringify({ username, password })
@@ -208,30 +172,48 @@
const data = await res.json(); const data = await res.json();
if (data.success) { if (data.success) {
showMessage("success", data.message || "Connexion réussie !"); msg.className = "message success";
window.location.href = "/dashboard"; msg.textContent = "Connexion réussie !";
window.location.href = "/dashboard";
} else { } else {
showMessage("error", data.message || "Identifiants incorrects."); msg.className = "message error";
msg.textContent = data.message || "Identifiants incorrects.";
} }
} catch { } catch {
showMessage("error", "Impossible de contacter le serveur."); msg.className = "message error";
msg.textContent = "Impossible de contacter le serveur.";
} finally { } finally {
btn.disabled = false; btn.disabled = false;
btn.classList.remove("loading");
btn.textContent = "Se connecter"; btn.textContent = "Se connecter";
} }
} }
function showMessage(type, text) { setInterval(async () => {
const msg = document.getElementById("msg"); try {
const path = document.getElementById("icon-path"); // Attention : Vérifie que cette route correspond bien à celle dans main.py
document.getElementById("msg-text").textContent = text; // (J'avais mis /check-rfid-login dans mon exemple précédent)
msg.className = "message " + type; const res = await fetch('/check-rfid-login');
path.setAttribute("d", type === "success" const data = await res.json();
? "M20 6L9 17l-5-5"
: "M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2zm0 6v4m0 4h.01" if (data.success) {
); const msg = document.getElementById("msg");
} const btn = document.getElementById("btn");
const inputs = document.querySelectorAll("input");
btn.disabled = true;
inputs.forEach(input => input.disabled = true);
msg.className = "message success";
msg.textContent = "Badge reconnu ! Bienvenue " + data.username + "...";
setTimeout(() => {
window.location.href = "/dashboard";
}, 1000);
}
} catch (e) {
// Erreurs ignorées silencieusement
}
}, 1500);
</script> </script>
</body> </body>
</html> </html>

View File

@@ -3,33 +3,14 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Logs — Loustiques</title> <title>Loustiques Home - Logs</title>
<link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
<style> <style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #0f0f0f;
--surface: #181818;
--surface2: #222222;
--border: rgba(255,255,255,0.08);
--border-hover: rgba(255,255,255,0.18);
--text: #f0ede8;
--muted: #888880;
--accent: #c8f060;
--accent-dim: rgba(200,240,96,0.12);
--danger: #ff5f57;
--danger-dim: rgba(255,95,87,0.12);
--success: #30d158;
--warning: #ff9f0a;
--mono: 'Space Mono', monospace;
--sans: 'DM Sans', sans-serif;
}
body { body {
background: var(--bg); font-family: system-ui, sans-serif;
color: var(--text); background: rgb(32, 31, 31);
font-family: var(--sans); color: white;
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
} }
@@ -37,8 +18,8 @@
aside { aside {
width: 220px; width: 220px;
min-height: 100vh; min-height: 100vh;
background: var(--surface); background: rgb(34, 34, 34);
border-right: 1px solid var(--border); border-right: 1px solid #2e2e2e;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 2rem 1.25rem; padding: 2rem 1.25rem;
@@ -47,22 +28,20 @@
} }
.logo { .logo {
font-family: var(--mono); font-size: 20px;
font-size: 13px; font-weight: 700;
color: var(--accent); color: white;
letter-spacing: 0.12em;
text-transform: uppercase;
margin-bottom: 2.5rem; margin-bottom: 2.5rem;
padding-bottom: 1.5rem; padding-bottom: 1.5rem;
border-bottom: 1px solid var(--border); border-bottom: 1px solid #2e2e2e;
} }
.logo span { .logo span {
display: block; display: block;
font-size: 10px; font-size: 15px;
color: var(--muted); color: gray;
margin-top: 4px; margin-top: 4px;
letter-spacing: 0.06em; font-weight: 400;
} }
nav a { nav a {
@@ -71,27 +50,26 @@
gap: 10px; gap: 10px;
padding: 8px 10px; padding: 8px 10px;
border-radius: 6px; border-radius: 6px;
font-size: 13.5px; font-size: 13px;
color: var(--muted); color: gray;
text-decoration: none; text-decoration: none;
margin-bottom: 2px; margin-bottom: 2px;
transition: all 0.15s; transition: all 0.15s;
} }
nav a.active, nav a:hover { nav a.active, nav a:hover {
background: var(--accent-dim); background: #2e2e3a;
color: var(--accent); color: #2563eb;
} }
nav a svg { width: 15px; height: 15px; flex-shrink: 0; } nav a svg { width: 15px; height: 15px; flex-shrink: 0; }
.sidebar-footer { .sidebar-footer {
margin-top: auto; margin-top: auto;
font-size: 12px; font-size: 15px;
color: var(--muted); color: #555;
font-family: var(--mono);
padding-top: 1rem; padding-top: 1rem;
border-top: 1px solid var(--border); border-top: 1px solid #2e2e2e;
} }
main { main {
@@ -107,40 +85,21 @@
margin-bottom: 2rem; margin-bottom: 2rem;
} }
.page-header h1 { .page-header h1 { font-size: 22px; font-weight: 700; margin-bottom: 4px; }
font-family: var(--mono); .page-header p { font-size: 14px; color: #888; }
font-size: 22px;
font-weight: 700;
color: var(--text);
margin-bottom: 4px;
}
.page-header p { font-size: 14px; color: var(--muted); } .controls { display: flex; align-items: center; gap: 10px; }
.controls {
display: flex;
align-items: center;
gap: 10px;
}
.live-dot { .live-dot {
width: 8px; width: 8px; height: 8px;
height: 8px;
border-radius: 50%; border-radius: 50%;
background: var(--success); background: #4ade80;
animation: pulse 2s infinite; animation: pulse 2s infinite;
} }
@keyframes pulse { @keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.live-label { .live-label { font-size: 11px; color: #666; }
font-family: var(--mono);
font-size: 11px;
color: var(--muted);
}
.btn { .btn {
display: inline-flex; display: inline-flex;
@@ -148,54 +107,42 @@
gap: 6px; gap: 6px;
padding: 7px 14px; padding: 7px 14px;
border-radius: 6px; border-radius: 6px;
border: 1px solid var(--border); border: 1px solid #3a3a3a;
font-family: var(--mono); font-family: inherit;
font-size: 11px; font-size: 12px;
letter-spacing: 0.05em;
cursor: pointer; cursor: pointer;
background: var(--surface); background: #2a2a2a;
color: var(--muted); color: #888;
transition: all 0.15s; transition: all 0.15s;
} }
.btn:hover { border-color: #555; color: #f0f0f0; }
.btn:hover { border-color: var(--border-hover); color: var(--text); } .btn-danger { color: #f87171; border-color: rgba(248,113,113,0.3); background: rgba(248,113,113,0.05); }
.btn-danger:hover { background: rgba(248,113,113,0.1); }
.btn-danger { .filters { display: flex; gap: 8px; margin-bottom: 1rem; }
color: var(--danger);
border-color: rgba(255,95,87,0.2);
background: var(--danger-dim);
}
.btn-danger:hover { background: rgba(255,95,87,0.2); }
.filters {
display: flex;
gap: 8px;
margin-bottom: 1rem;
}
.filter-btn { .filter-btn {
padding: 5px 12px; padding: 5px 12px;
border-radius: 5px; border-radius: 5px;
border: 1px solid var(--border); border: 1px solid #3a3a3a;
background: transparent; background: transparent;
font-family: var(--mono); font-family: inherit;
font-size: 11px; font-size: 11px;
color: var(--muted); color: #888;
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
letter-spacing: 0.05em;
} }
.filter-btn.active { background: var(--accent-dim); color: var(--accent); border-color: rgba(200,240,96,0.25); } .filter-btn.active { background: #2e2e3a; color: #2563eb; border-color: rgba(37,99,235,0.3); }
.filter-btn[data-level="ERROR"].active { background: var(--danger-dim); color: var(--danger); border-color: rgba(255,95,87,0.25); } .filter-btn[data-level="ERROR"].active { background: rgba(248,113,113,0.1); color: #f87171; border-color: rgba(248,113,113,0.3); }
.filter-btn[data-level="WARNING"].active { background: rgba(255,159,10,0.1); color: var(--warning); border-color: rgba(255,159,10,0.25); } .filter-btn[data-level="WARNING"].active { background: rgba(251,191,36,0.1); color: #fbbf24; border-color: rgba(251,191,36,0.3); }
.filter-btn[data-level="INFO"].active { background: rgba(48,209,88,0.08); color: var(--success); border-color: rgba(48,209,88,0.25); } .filter-btn[data-level="INFO"].active { background: rgba(74,222,128,0.08); color: #4ade80; border-color: rgba(74,222,128,0.3); }
.log-container { .log-container {
background: var(--surface); background: #2a2a2a;
border: 1px solid var(--border); border: 1px solid #333;
border-radius: 12px; border-radius: 8px;
overflow: hidden; overflow: hidden;
} }
@@ -203,43 +150,36 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px 16px; padding: 10px 16px;
border-bottom: 1px solid var(--border); border-bottom: 1px solid #333;
} }
.log-count { .log-count { font-size: 11px; color: #666; }
font-family: var(--mono);
font-size: 11px;
color: var(--muted);
}
.log-scroll { .log-scroll {
height: calc(100vh - 280px); height: calc(100vh - 280px);
overflow-y: auto; overflow-y: auto;
padding: 0; font-family: monospace;
} }
.log-scroll::-webkit-scrollbar { width: 4px; } .log-scroll::-webkit-scrollbar { width: 4px; }
.log-scroll::-webkit-scrollbar-track { background: transparent; } .log-scroll::-webkit-scrollbar-track { background: transparent; }
.log-scroll::-webkit-scrollbar-thumb { background: var(--border-hover); border-radius: 2px; } .log-scroll::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }
.log-line { .log-line {
display: grid; display: grid;
grid-template-columns: 160px 70px 80px 1fr; grid-template-columns: 160px 70px 80px 1fr;
gap: 0;
padding: 7px 16px; padding: 7px 16px;
border-bottom: 1px solid rgba(255,255,255,0.03); border-bottom: 1px solid rgba(255,255,255,0.03);
font-family: var(--mono);
font-size: 12px; font-size: 12px;
transition: background 0.1s;
align-items: center; align-items: center;
transition: background 0.1s;
} }
.log-line:hover { background: #333; }
.log-line:hover { background: var(--surface2); }
.log-line:last-child { border-bottom: none; } .log-line:last-child { border-bottom: none; }
.log-time { color: var(--muted); font-size: 11px; } .log-time { color: #666; font-size: 11px; }
.log-name { color: var(--muted); font-size: 11px; } .log-name { color: #666; font-size: 11px; }
.log-level { .log-level {
font-size: 10px; font-size: 10px;
@@ -247,15 +187,14 @@
border-radius: 4px; border-radius: 4px;
text-align: center; text-align: center;
width: fit-content; width: fit-content;
letter-spacing: 0.06em;
} }
.level-INFO { background: rgba(48,209,88,0.1); color: var(--success); } .level-INFO { background: rgba(74,222,128,0.1); color: #4ade80; }
.level-ERROR { background: var(--danger-dim); color: var(--danger); } .level-ERROR { background: rgba(248,113,113,0.1); color: #f87171; }
.level-WARNING { background: rgba(255,159,10,0.1); color: var(--warning); } .level-WARNING { background: rgba(251,191,36,0.1); color: #fbbf24; }
.level-DEBUG { background: var(--surface2); color: var(--muted); } .level-DEBUG { background: #333; color: #666; }
.log-msg { color: var(--text); padding-left: 12px; word-break: break-all; } .log-msg { color: #f0f0f0; padding-left: 12px; word-break: break-all; }
.empty-state { .empty-state {
display: flex; display: flex;
@@ -263,35 +202,33 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
height: 200px; height: 200px;
color: var(--muted); color: #555;
font-family: var(--mono); font-size: 13px;
font-size: 12px;
gap: 8px; gap: 8px;
} }
.search-input { .search-input {
background: var(--surface2); background: #333;
border: 1px solid var(--border); border: 1px solid #3a3a3a;
border-radius: 6px; border-radius: 6px;
padding: 6px 12px; padding: 6px 12px;
color: var(--text); color: #f0f0f0;
font-family: var(--mono); font-family: inherit;
font-size: 12px; font-size: 12px;
outline: none; outline: none;
width: 220px; width: 220px;
transition: border-color 0.15s; transition: border-color 0.15s;
} }
.search-input:focus { border-color: #2563eb; }
.search-input:focus { border-color: var(--accent); } .search-input::placeholder { color: #555; }
.search-input::placeholder { color: var(--muted); }
</style> </style>
</head> </head>
<body> <body>
<aside> <aside>
<div class="logo"> <div class="logo">
Loustiques SUPER Loustiques
<span>panneau admin</span> <span>Panneau admin</span>
</div> </div>
<nav> <nav>
<a href="/admin"> <a href="/admin">
@@ -302,21 +239,16 @@
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="12" height="12" rx="2"/><path d="M5 8h6M5 5h6M5 11h4"/></svg> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2" y="2" width="12" height="12" rx="2"/><path d="M5 8h6M5 5h6M5 11h4"/></svg>
Logs Logs
</a> </a>
<a href="#">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="3"/><path d="M8 1v2M8 13v2M1 8h2M13 8h2"/></svg>
Système
</a>
</nav> </nav>
<div class="sidebar-footer"> <div class="sidebar-footer">Version 1.0</div>
v0.1.0 — local
</div>
</aside> </aside>
<main> <main>
<div class="page-header"> <div class="page-header">
<div> <div>
<h1>logs</h1> <h1>Logs</h1>
<p>Flux en temps réel de <code style="font-family:var(--mono);font-size:12px;color:var(--muted)">/var/log/loustique.log</code></p> <p>Flux en temps réel de <code style="font-size:12px;color:#888">/var/log/loustique.log</code></p>
</div> </div>
<div class="controls"> <div class="controls">
<div class="live-dot" id="live-dot"></div> <div class="live-dot" id="live-dot"></div>
@@ -441,13 +373,13 @@ function toggleLive() {
const btn = document.getElementById('toggle-label'); const btn = document.getElementById('toggle-label');
if (liveEnabled) { if (liveEnabled) {
dot.style.animationPlayState = 'running'; dot.style.animationPlayState = 'running';
dot.style.background = 'var(--success)'; dot.style.background = '#4ade80';
label.textContent = 'live'; label.textContent = 'live';
btn.textContent = 'Pause'; btn.textContent = 'Pause';
startInterval(); startInterval();
} else { } else {
dot.style.animationPlayState = 'paused'; dot.style.animationPlayState = 'paused';
dot.style.background = 'var(--muted)'; dot.style.background = '#555';
label.textContent = 'pausé'; label.textContent = 'pausé';
btn.textContent = 'Reprendre'; btn.textContent = 'Reprendre';
clearInterval(interval); clearInterval(interval);

16
to_do.txt Normal file
View File

@@ -0,0 +1,16 @@
1. ldr --> ETEINT ALLUME ou lux && activer les trois led (phyisque si possible faire en sorte d'eteindre les
lumières via html faire bouton auto ou manuel sur le html pour ldr)
2. Servo moteur phyisquement ouvrir/fermer et afficher l'etat sur l'html div presentoire
(36/13 convertir en BCM)
3. Faire la logique de l'alarme et que le peer detecte du mouvement ca sonne + RGB alarme
- bleu desactivée
- Vert activée
- Rouge activée et détection d'objet
activation du buzzer de façon COHÉRENTE !!!!!
4. RFID à vérifier
5. faire la connectivité des raspberries

View File

@@ -1,247 +0,0 @@
<#
.Synopsis
Activate a Python virtual environment for the current PowerShell session.
.Description
Pushes the python executable for a virtual environment to the front of the
$Env:PATH environment variable and sets the prompt to signify that you are
in a Python virtual environment. Makes use of the command line switches as
well as the `pyvenv.cfg` file values present in the virtual environment.
.Parameter VenvDir
Path to the directory that contains the virtual environment to activate. The
default value for this is the parent of the directory that the Activate.ps1
script is located within.
.Parameter Prompt
The prompt prefix to display when this virtual environment is activated. By
default, this prompt is the name of the virtual environment folder (VenvDir)
surrounded by parentheses and followed by a single space (ie. '(.venv) ').
.Example
Activate.ps1
Activates the Python virtual environment that contains the Activate.ps1 script.
.Example
Activate.ps1 -Verbose
Activates the Python virtual environment that contains the Activate.ps1 script,
and shows extra information about the activation as it executes.
.Example
Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv
Activates the Python virtual environment located in the specified location.
.Example
Activate.ps1 -Prompt "MyPython"
Activates the Python virtual environment that contains the Activate.ps1 script,
and prefixes the current prompt with the specified string (surrounded in
parentheses) while the virtual environment is active.
.Notes
On Windows, it may be required to enable this Activate.ps1 script by setting the
execution policy for the user. You can do this by issuing the following PowerShell
command:
PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
For more information on Execution Policies:
https://go.microsoft.com/fwlink/?LinkID=135170
#>
Param(
[Parameter(Mandatory = $false)]
[String]
$VenvDir,
[Parameter(Mandatory = $false)]
[String]
$Prompt
)
<# Function declarations --------------------------------------------------- #>
<#
.Synopsis
Remove all shell session elements added by the Activate script, including the
addition of the virtual environment's Python executable from the beginning of
the PATH variable.
.Parameter NonDestructive
If present, do not remove this function from the global namespace for the
session.
#>
function global:deactivate ([switch]$NonDestructive) {
# Revert to original values
# The prior prompt:
if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) {
Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt
Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT
}
# The prior PYTHONHOME:
if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) {
Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME
Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME
}
# The prior PATH:
if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) {
Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH
Remove-Item -Path Env:_OLD_VIRTUAL_PATH
}
# Just remove the VIRTUAL_ENV altogether:
if (Test-Path -Path Env:VIRTUAL_ENV) {
Remove-Item -Path env:VIRTUAL_ENV
}
# Just remove VIRTUAL_ENV_PROMPT altogether.
if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) {
Remove-Item -Path env:VIRTUAL_ENV_PROMPT
}
# Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether:
if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) {
Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force
}
# Leave deactivate function in the global namespace if requested:
if (-not $NonDestructive) {
Remove-Item -Path function:deactivate
}
}
<#
.Description
Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the
given folder, and returns them in a map.
For each line in the pyvenv.cfg file, if that line can be parsed into exactly
two strings separated by `=` (with any amount of whitespace surrounding the =)
then it is considered a `key = value` line. The left hand string is the key,
the right hand is the value.
If the value starts with a `'` or a `"` then the first and last character is
stripped from the value before being captured.
.Parameter ConfigDir
Path to the directory that contains the `pyvenv.cfg` file.
#>
function Get-PyVenvConfig(
[String]
$ConfigDir
) {
Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg"
# Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue).
$pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue
# An empty map will be returned if no config file is found.
$pyvenvConfig = @{ }
if ($pyvenvConfigPath) {
Write-Verbose "File exists, parse `key = value` lines"
$pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath
$pyvenvConfigContent | ForEach-Object {
$keyval = $PSItem -split "\s*=\s*", 2
if ($keyval[0] -and $keyval[1]) {
$val = $keyval[1]
# Remove extraneous quotations around a string value.
if ("'""".Contains($val.Substring(0, 1))) {
$val = $val.Substring(1, $val.Length - 2)
}
$pyvenvConfig[$keyval[0]] = $val
Write-Verbose "Adding Key: '$($keyval[0])'='$val'"
}
}
}
return $pyvenvConfig
}
<# Begin Activate script --------------------------------------------------- #>
# Determine the containing directory of this script
$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$VenvExecDir = Get-Item -Path $VenvExecPath
Write-Verbose "Activation script is located in path: '$VenvExecPath'"
Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)"
Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)"
# Set values required in priority: CmdLine, ConfigFile, Default
# First, get the location of the virtual environment, it might not be
# VenvExecDir if specified on the command line.
if ($VenvDir) {
Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values"
}
else {
Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir."
$VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/")
Write-Verbose "VenvDir=$VenvDir"
}
# Next, read the `pyvenv.cfg` file to determine any required value such
# as `prompt`.
$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir
# Next, set the prompt from the command line, or the config file, or
# just use the name of the virtual environment folder.
if ($Prompt) {
Write-Verbose "Prompt specified as argument, using '$Prompt'"
}
else {
Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value"
if ($pyvenvCfg -and $pyvenvCfg['prompt']) {
Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'"
$Prompt = $pyvenvCfg['prompt'];
}
else {
Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)"
Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'"
$Prompt = Split-Path -Path $venvDir -Leaf
}
}
Write-Verbose "Prompt = '$Prompt'"
Write-Verbose "VenvDir='$VenvDir'"
# Deactivate any currently active virtual environment, but leave the
# deactivate function in place.
deactivate -nondestructive
# Now set the environment variable VIRTUAL_ENV, used by many tools to determine
# that there is an activated venv.
$env:VIRTUAL_ENV = $VenvDir
if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) {
Write-Verbose "Setting prompt to '$Prompt'"
# Set the prompt to include the env name
# Make sure _OLD_VIRTUAL_PROMPT is global
function global:_OLD_VIRTUAL_PROMPT { "" }
Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT
New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt
function global:prompt {
Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) "
_OLD_VIRTUAL_PROMPT
}
$env:VIRTUAL_ENV_PROMPT = $Prompt
}
# Clear PYTHONHOME
if (Test-Path -Path Env:PYTHONHOME) {
Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME
Remove-Item -Path Env:PYTHONHOME
}
# Add the venv to the PATH
Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH
$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH"

View File

@@ -1,69 +0,0 @@
# This file must be used with "source bin/activate" *from bash*
# you cannot run it directly
deactivate () {
# reset old environment variables
if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then
PATH="${_OLD_VIRTUAL_PATH:-}"
export PATH
unset _OLD_VIRTUAL_PATH
fi
if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then
PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}"
export PYTHONHOME
unset _OLD_VIRTUAL_PYTHONHOME
fi
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
hash -r 2> /dev/null
fi
if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then
PS1="${_OLD_VIRTUAL_PS1:-}"
export PS1
unset _OLD_VIRTUAL_PS1
fi
unset VIRTUAL_ENV
unset VIRTUAL_ENV_PROMPT
if [ ! "${1:-}" = "nondestructive" ] ; then
# Self destruct!
unset -f deactivate
fi
}
# unset irrelevant variables
deactivate nondestructive
VIRTUAL_ENV=/home/maxime/Documents/loustiques-home/venv
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/"bin":$PATH"
export PATH
# unset PYTHONHOME if set
# this will fail if PYTHONHOME is set to the empty string (which is bad anyway)
# could use `if (set -u; : $PYTHONHOME) ;` in bash
if [ -n "${PYTHONHOME:-}" ] ; then
_OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}"
unset PYTHONHOME
fi
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
_OLD_VIRTUAL_PS1="${PS1:-}"
PS1='(venv) '"${PS1:-}"
export PS1
VIRTUAL_ENV_PROMPT='(venv) '
export VIRTUAL_ENV_PROMPT
fi
# This should detect bash and zsh, which have a hash command that must
# be called to get it to forget past commands. Without forgetting
# past commands the $PATH changes we made may not be respected
if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then
hash -r 2> /dev/null
fi

View File

@@ -1,26 +0,0 @@
# This file must be used with "source bin/activate.csh" *from csh*.
# You cannot run it directly.
# Created by Davide Di Blasi <davidedb@gmail.com>.
# Ported to Python 3.3 venv by Andrew Svetlov <andrew.svetlov@gmail.com>
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate'
# Unset irrelevant variables.
deactivate nondestructive
setenv VIRTUAL_ENV /home/maxime/Documents/loustiques-home/venv
set _OLD_VIRTUAL_PATH="$PATH"
setenv PATH "$VIRTUAL_ENV/"bin":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
set prompt = '(venv) '"$prompt"
setenv VIRTUAL_ENV_PROMPT '(venv) '
endif
alias pydoc python -m pydoc
rehash

View File

@@ -1,69 +0,0 @@
# This file must be used with "source <venv>/bin/activate.fish" *from fish*
# (https://fishshell.com/); you cannot run it directly.
function deactivate -d "Exit virtual environment and return to normal shell environment"
# reset old environment variables
if test -n "$_OLD_VIRTUAL_PATH"
set -gx PATH $_OLD_VIRTUAL_PATH
set -e _OLD_VIRTUAL_PATH
end
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME
set -e _OLD_VIRTUAL_PYTHONHOME
end
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
set -e _OLD_FISH_PROMPT_OVERRIDE
# prevents error when using nested fish instances (Issue #93858)
if functions -q _old_fish_prompt
functions -e fish_prompt
functions -c _old_fish_prompt fish_prompt
functions -e _old_fish_prompt
end
end
set -e VIRTUAL_ENV
set -e VIRTUAL_ENV_PROMPT
if test "$argv[1]" != "nondestructive"
# Self-destruct!
functions -e deactivate
end
end
# Unset irrelevant variables.
deactivate nondestructive
set -gx VIRTUAL_ENV /home/maxime/Documents/loustiques-home/venv
set -gx _OLD_VIRTUAL_PATH $PATH
set -gx PATH "$VIRTUAL_ENV/"bin $PATH
# Unset PYTHONHOME if set.
if set -q PYTHONHOME
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
set -e PYTHONHOME
end
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
# fish uses a function instead of an env var to generate the prompt.
# Save the current fish_prompt function as the function _old_fish_prompt.
functions -c fish_prompt _old_fish_prompt
# With the original prompt function renamed, we can override with our own.
function fish_prompt
# Save the return status of the last command.
set -l old_status $status
# Output the venv prompt; color taken from the blue of the Python logo.
printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal)
# Restore the return status of the previous command.
echo "exit $old_status" | .
# Output the original/"old" prompt.
_old_fish_prompt
end
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
set -gx VIRTUAL_ENV_PROMPT '(venv) '
end

View File

@@ -1,8 +0,0 @@
#!/home/maxime/Documents/loustiques-home/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from dotenv.__main__ import cli
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(cli())

View File

@@ -1,8 +0,0 @@
#!/home/maxime/Documents/loustiques-home/venv/bin/python
# -*- coding: utf-8 -*-
import re
import sys
from flask.cli import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@@ -1,8 +0,0 @@
#!/home/maxime/Documents/loustiques-home/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from gpiozerocli.pinout import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@@ -1,8 +0,0 @@
#!/home/maxime/Documents/loustiques-home/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from gpiozerocli.pintest import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@@ -1,8 +0,0 @@
#!/home/maxime/Documents/loustiques-home/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@@ -1,8 +0,0 @@
#!/home/maxime/Documents/loustiques-home/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@@ -1,8 +0,0 @@
#!/home/maxime/Documents/loustiques-home/venv/bin/python3
# -*- coding: utf-8 -*-
import re
import sys
from pip._internal.cli.main import main
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main())

View File

@@ -1 +0,0 @@
python3

View File

@@ -1 +0,0 @@
/usr/bin/python3

View File

@@ -1 +0,0 @@
python3

View File

@@ -1,222 +0,0 @@
# don't import any costly modules
import sys
import os
is_pypy = '__pypy__' in sys.builtin_module_names
def warn_distutils_present():
if 'distutils' not in sys.modules:
return
if is_pypy and sys.version_info < (3, 7):
# PyPy for 3.6 unconditionally imports distutils, so bypass the warning
# https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
return
import warnings
warnings.warn(
"Distutils was imported before Setuptools, but importing Setuptools "
"also replaces the `distutils` module in `sys.modules`. This may lead "
"to undesirable behaviors or errors. To avoid these issues, avoid "
"using distutils directly, ensure that setuptools is installed in the "
"traditional way (e.g. not an editable install), and/or make sure "
"that setuptools is always imported before distutils."
)
def clear_distutils():
if 'distutils' not in sys.modules:
return
import warnings
warnings.warn("Setuptools is replacing distutils.")
mods = [
name
for name in sys.modules
if name == "distutils" or name.startswith("distutils.")
]
for name in mods:
del sys.modules[name]
def enabled():
"""
Allow selection of distutils by environment variable.
"""
which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
return which == 'local'
def ensure_local_distutils():
import importlib
clear_distutils()
# With the DistutilsMetaFinder in place,
# perform an import to cause distutils to be
# loaded from setuptools._distutils. Ref #2906.
with shim():
importlib.import_module('distutils')
# check that submodules load as expected
core = importlib.import_module('distutils.core')
assert '_distutils' in core.__file__, core.__file__
assert 'setuptools._distutils.log' not in sys.modules
def do_override():
"""
Ensure that the local copy of distutils is preferred over stdlib.
See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
for more motivation.
"""
if enabled():
warn_distutils_present()
ensure_local_distutils()
class _TrivialRe:
def __init__(self, *patterns):
self._patterns = patterns
def match(self, string):
return all(pat in string for pat in self._patterns)
class DistutilsMetaFinder:
def find_spec(self, fullname, path, target=None):
# optimization: only consider top level modules and those
# found in the CPython test suite.
if path is not None and not fullname.startswith('test.'):
return
method_name = 'spec_for_{fullname}'.format(**locals())
method = getattr(self, method_name, lambda: None)
return method()
def spec_for_distutils(self):
if self.is_cpython():
return
import importlib
import importlib.abc
import importlib.util
try:
mod = importlib.import_module('setuptools._distutils')
except Exception:
# There are a couple of cases where setuptools._distutils
# may not be present:
# - An older Setuptools without a local distutils is
# taking precedence. Ref #2957.
# - Path manipulation during sitecustomize removes
# setuptools from the path but only after the hook
# has been loaded. Ref #2980.
# In either case, fall back to stdlib behavior.
return
class DistutilsLoader(importlib.abc.Loader):
def create_module(self, spec):
mod.__name__ = 'distutils'
return mod
def exec_module(self, module):
pass
return importlib.util.spec_from_loader(
'distutils', DistutilsLoader(), origin=mod.__file__
)
@staticmethod
def is_cpython():
"""
Suppress supplying distutils for CPython (build and tests).
Ref #2965 and #3007.
"""
return os.path.isfile('pybuilddir.txt')
def spec_for_pip(self):
"""
Ensure stdlib distutils when running under pip.
See pypa/pip#8761 for rationale.
"""
if self.pip_imported_during_build():
return
clear_distutils()
self.spec_for_distutils = lambda: None
@classmethod
def pip_imported_during_build(cls):
"""
Detect if pip is being imported in a build script. Ref #2355.
"""
import traceback
return any(
cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
)
@staticmethod
def frame_file_is_setup(frame):
"""
Return True if the indicated frame suggests a setup.py file.
"""
# some frames may not have __file__ (#2940)
return frame.f_globals.get('__file__', '').endswith('setup.py')
def spec_for_sensitive_tests(self):
"""
Ensure stdlib distutils when running select tests under CPython.
python/cpython#91169
"""
clear_distutils()
self.spec_for_distutils = lambda: None
sensitive_tests = (
[
'test.test_distutils',
'test.test_peg_generator',
'test.test_importlib',
]
if sys.version_info < (3, 10)
else [
'test.test_distutils',
]
)
for name in DistutilsMetaFinder.sensitive_tests:
setattr(
DistutilsMetaFinder,
f'spec_for_{name}',
DistutilsMetaFinder.spec_for_sensitive_tests,
)
DISTUTILS_FINDER = DistutilsMetaFinder()
def add_shim():
DISTUTILS_FINDER in sys.meta_path or insert_shim()
class shim:
def __enter__(self):
insert_shim()
def __exit__(self, exc, value, tb):
remove_shim()
def insert_shim():
sys.meta_path.insert(0, DISTUTILS_FINDER)
def remove_shim():
try:
sys.meta_path.remove(DISTUTILS_FINDER)
except ValueError:
pass

View File

@@ -1 +0,0 @@
__import__('_distutils_hack').do_override()

View File

@@ -1,343 +0,0 @@
Metadata-Version: 2.4
Name: bcrypt
Version: 5.0.0
Summary: Modern password hashing for your software and your servers
Author-email: The Python Cryptographic Authority developers <cryptography-dev@python.org>
License: Apache-2.0
Project-URL: homepage, https://github.com/pyca/bcrypt/
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable
Requires-Python: >=3.8
Description-Content-Type: text/x-rst
License-File: LICENSE
Provides-Extra: tests
Requires-Dist: pytest!=3.3.0,>=3.2.1; extra == "tests"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file
bcrypt
======
.. image:: https://img.shields.io/pypi/v/bcrypt.svg
:target: https://pypi.org/project/bcrypt/
:alt: Latest Version
.. image:: https://github.com/pyca/bcrypt/workflows/CI/badge.svg?branch=main
:target: https://github.com/pyca/bcrypt/actions?query=workflow%3ACI+branch%3Amain
Acceptable password hashing for your software and your servers (but you should
really use argon2id or scrypt)
Installation
============
To install bcrypt, simply:
.. code:: console
$ pip install bcrypt
Note that bcrypt should build very easily on Linux provided you have a C
compiler and a Rust compiler (the minimum supported Rust version is 1.56.0).
For Debian and Ubuntu, the following command will ensure that the required dependencies are installed:
.. code:: console
$ sudo apt-get install build-essential cargo
For Fedora and RHEL-derivatives, the following command will ensure that the required dependencies are installed:
.. code:: console
$ sudo yum install gcc cargo
For Alpine, the following command will ensure that the required dependencies are installed:
.. code:: console
$ apk add --update musl-dev gcc cargo
Alternatives
============
While bcrypt remains an acceptable choice for password storage, depending on your specific use case you may also want to consider using scrypt (either via `standard library`_ or `cryptography`_) or argon2id via `argon2_cffi`_.
Changelog
=========
5.0.0
-----
* Bumped MSRV to 1.74.
* Added support for Python 3.14 and free-threaded Python 3.14.
* Added support for Windows on ARM.
* Passing ``hashpw`` a password longer than 72 bytes now raises a
``ValueError``. Previously the password was silently truncated, following the
behavior of the original OpenBSD ``bcrypt`` implementation.
4.3.0
-----
* Dropped support for Python 3.7.
* We now support free-threaded Python 3.13.
* We now support PyPy 3.11.
* We now publish wheels for free-threaded Python 3.13, for PyPy 3.11 on
``manylinux``, and for ARMv7l on ``manylinux``.
4.2.1
-----
* Bump Rust dependency versions - this should resolve crashes on Python 3.13
free-threaded builds.
* We no longer build ``manylinux`` wheels for PyPy 3.9.
4.2.0
-----
* Bump Rust dependency versions
* Removed the ``BCRYPT_ALLOW_RUST_163`` environment variable.
4.1.3
-----
* Bump Rust dependency versions
4.1.2
-----
* Publish both ``py37`` and ``py39`` wheels. This should resolve some errors
relating to initializing a module multiple times per process.
4.1.1
-----
* Fixed the type signature on the ``kdf`` method.
* Fixed packaging bug on Windows.
* Fixed incompatibility with passlib package detection assumptions.
4.1.0
-----
* Dropped support for Python 3.6.
* Bumped MSRV to 1.64. (Note: Rust 1.63 can be used by setting the ``BCRYPT_ALLOW_RUST_163`` environment variable)
4.0.1
-----
* We now build PyPy ``manylinux`` wheels.
* Fixed a bug where passing an invalid ``salt`` to ``checkpw`` could result in
a ``pyo3_runtime.PanicException``. It now correctly raises a ``ValueError``.
4.0.0
-----
* ``bcrypt`` is now implemented in Rust. Users building from source will need
to have a Rust compiler available. Nothing will change for users downloading
wheels.
* We no longer ship ``manylinux2010`` wheels. Users should upgrade to the latest
``pip`` to ensure this doesnt cause issues downloading wheels on their
platform. We now ship ``manylinux_2_28`` wheels for users on new enough platforms.
* ``NUL`` bytes are now allowed in inputs.
3.2.2
-----
* Fixed packaging of ``py.typed`` files in wheels so that ``mypy`` works.
3.2.1
-----
* Added support for compilation on z/OS
* The next release of ``bcrypt`` with be 4.0 and it will require Rust at
compile time, for users building from source. There will be no additional
requirement for users who are installing from wheels. Users on most
platforms will be able to obtain a wheel by making sure they have an up to
date ``pip``. The minimum supported Rust version will be 1.56.0.
* This will be the final release for which we ship ``manylinux2010`` wheels.
Going forward the minimum supported manylinux ABI for our wheels will be
``manylinux2014``. The vast majority of users will continue to receive
``manylinux`` wheels provided they have an up to date ``pip``.
3.2.0
-----
* Added typehints for library functions.
* Dropped support for Python versions less than 3.6 (2.7, 3.4, 3.5).
* Shipped ``abi3`` Windows wheels (requires pip >= 20).
3.1.7
-----
* Set a ``setuptools`` lower bound for PEP517 wheel building.
* We no longer distribute 32-bit ``manylinux1`` wheels. Continuing to produce
them was a maintenance burden.
3.1.6
-----
* Added support for compilation on Haiku.
3.1.5
-----
* Added support for compilation on AIX.
* Dropped Python 2.6 and 3.3 support.
* Switched to using ``abi3`` wheels for Python 3. If you are not getting a
wheel on a compatible platform please upgrade your ``pip`` version.
3.1.4
-----
* Fixed compilation with mingw and on illumos.
3.1.3
-----
* Fixed a compilation issue on Solaris.
* Added a warning when using too few rounds with ``kdf``.
3.1.2
-----
* Fixed a compile issue affecting big endian platforms.
* Fixed invalid escape sequence warnings on Python 3.6.
* Fixed building in non-UTF8 environments on Python 2.
3.1.1
-----
* Resolved a ``UserWarning`` when used with ``cffi`` 1.8.3.
3.1.0
-----
* Added support for ``checkpw``, a convenience method for verifying a password.
* Ensure that you get a ``$2y$`` hash when you input a ``$2y$`` salt.
* Fixed a regression where ``$2a`` hashes were vulnerable to a wraparound bug.
* Fixed compilation under Alpine Linux.
3.0.0
-----
* Switched the C backend to code obtained from the OpenBSD project rather than
openwall.
* Added support for ``bcrypt_pbkdf`` via the ``kdf`` function.
2.0.0
-----
* Added support for an adjustible prefix when calling ``gensalt``.
* Switched to CFFI 1.0+
Usage
-----
Password Hashing
~~~~~~~~~~~~~~~~
Hashing and then later checking that a password matches the previous hashed
password is very simple:
.. code:: pycon
>>> import bcrypt
>>> password = b"super secret password"
>>> # Hash a password for the first time, with a randomly-generated salt
>>> hashed = bcrypt.hashpw(password, bcrypt.gensalt())
>>> # Check that an unhashed password matches one that has previously been
>>> # hashed
>>> if bcrypt.checkpw(password, hashed):
... print("It Matches!")
... else:
... print("It Does not Match :(")
KDF
~~~
As of 3.0.0 ``bcrypt`` now offers a ``kdf`` function which does ``bcrypt_pbkdf``.
This KDF is used in OpenSSH's newer encrypted private key format.
.. code:: pycon
>>> import bcrypt
>>> key = bcrypt.kdf(
... password=b'password',
... salt=b'salt',
... desired_key_bytes=32,
... rounds=100)
Adjustable Work Factor
~~~~~~~~~~~~~~~~~~~~~~
One of bcrypt's features is an adjustable logarithmic work factor. To adjust
the work factor merely pass the desired number of rounds to
``bcrypt.gensalt(rounds=12)`` which defaults to 12):
.. code:: pycon
>>> import bcrypt
>>> password = b"super secret password"
>>> # Hash a password for the first time, with a certain number of rounds
>>> hashed = bcrypt.hashpw(password, bcrypt.gensalt(14))
>>> # Check that a unhashed password matches one that has previously been
>>> # hashed
>>> if bcrypt.checkpw(password, hashed):
... print("It Matches!")
... else:
... print("It Does not Match :(")
Adjustable Prefix
~~~~~~~~~~~~~~~~~
Another one of bcrypt's features is an adjustable prefix to let you define what
libraries you'll remain compatible with. To adjust this, pass either ``2a`` or
``2b`` (the default) to ``bcrypt.gensalt(prefix=b"2b")`` as a bytes object.
As of 3.0.0 the ``$2y$`` prefix is still supported in ``hashpw`` but deprecated.
Maximum Password Length
~~~~~~~~~~~~~~~~~~~~~~~
The bcrypt algorithm only handles passwords up to 72 characters, any characters
beyond that are ignored. To work around this, a common approach is to hash a
password with a cryptographic hash (such as ``sha256``) and then base64
encode it to prevent NULL byte problems before hashing the result with
``bcrypt``:
.. code:: pycon
>>> password = b"an incredibly long password" * 10
>>> hashed = bcrypt.hashpw(
... base64.b64encode(hashlib.sha256(password).digest()),
... bcrypt.gensalt()
... )
Compatibility
-------------
This library should be compatible with py-bcrypt and it will run on Python
3.8+ (including free-threaded builds), and PyPy 3.
Security
--------
``bcrypt`` follows the `same security policy as cryptography`_, if you
identify a vulnerability, we ask you to contact us privately.
.. _`same security policy as cryptography`: https://cryptography.io/en/latest/security.html
.. _`standard library`: https://docs.python.org/3/library/hashlib.html#hashlib.scrypt
.. _`argon2_cffi`: https://argon2-cffi.readthedocs.io
.. _`cryptography`: https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#cryptography.hazmat.primitives.kdf.scrypt.Scrypt

View File

@@ -1,12 +0,0 @@
bcrypt-5.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
bcrypt-5.0.0.dist-info/METADATA,sha256=yV1BfLlI6udlVy23eNbzDa62DSEbUrlWvlLBCI6UAdI,10524
bcrypt-5.0.0.dist-info/RECORD,,
bcrypt-5.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
bcrypt-5.0.0.dist-info/WHEEL,sha256=WieEZvWpc0Erab6-NfTu9412g-GcE58js6gvBn3Q7B4,111
bcrypt-5.0.0.dist-info/licenses/LICENSE,sha256=gXPVwptPlW1TJ4HSuG5OMPg-a3h43OGMkZRR1rpwfJA,10850
bcrypt-5.0.0.dist-info/top_level.txt,sha256=BkR_qBzDbSuycMzHWE1vzXrfYecAzUVmQs6G2CukqNI,7
bcrypt/__init__.py,sha256=cv-NupIX6P7o6A4PK_F0ur6IZoDr3GnvyzFO9k16wKQ,1000
bcrypt/__init__.pyi,sha256=ITUCB9mPVU8sKUbJQMDUH5YfQXZb1O55F9qvKZR_o8I,333
bcrypt/__pycache__/__init__.cpython-311.pyc,,
bcrypt/_bcrypt.abi3.so,sha256=oFwJu4Gq44FqJDttx_oWpypfuUQ30BkCWzD2FhojdYw,631768
bcrypt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0

View File

@@ -1,5 +0,0 @@
Wheel-Version: 1.0
Generator: setuptools (80.9.0)
Root-Is-Purelib: false
Tag: cp39-abi3-manylinux_2_34_x86_64

View File

@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,43 +0,0 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ._bcrypt import (
__author__,
__copyright__,
__email__,
__license__,
__summary__,
__title__,
__uri__,
checkpw,
gensalt,
hashpw,
kdf,
)
from ._bcrypt import (
__version_ex__ as __version__,
)
__all__ = [
"__author__",
"__copyright__",
"__email__",
"__license__",
"__summary__",
"__title__",
"__uri__",
"__version__",
"checkpw",
"gensalt",
"hashpw",
"kdf",
]

View File

@@ -1,10 +0,0 @@
def gensalt(rounds: int = 12, prefix: bytes = b"2b") -> bytes: ...
def hashpw(password: bytes, salt: bytes) -> bytes: ...
def checkpw(password: bytes, hashed_password: bytes) -> bool: ...
def kdf(
password: bytes,
salt: bytes,
desired_key_bytes: int,
rounds: int,
ignore_few_rounds: bool = False,
) -> bytes: ...

View File

@@ -1,20 +0,0 @@
Copyright 2010 Jason Kirtland
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@@ -1,60 +0,0 @@
Metadata-Version: 2.3
Name: blinker
Version: 1.9.0
Summary: Fast, simple object-to-object and broadcast signaling
Author: Jason Kirtland
Maintainer-email: Pallets Ecosystem <contact@palletsprojects.com>
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Classifier: Development Status :: 5 - Production/Stable
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Typing :: Typed
Project-URL: Chat, https://discord.gg/pallets
Project-URL: Documentation, https://blinker.readthedocs.io
Project-URL: Source, https://github.com/pallets-eco/blinker/
# Blinker
Blinker provides a fast dispatching system that allows any number of
interested parties to subscribe to events, or "signals".
## Pallets Community Ecosystem
> [!IMPORTANT]\
> This project is part of the Pallets Community Ecosystem. Pallets is the open
> source organization that maintains Flask; Pallets-Eco enables community
> maintenance of related projects. If you are interested in helping maintain
> this project, please reach out on [the Pallets Discord server][discord].
>
> [discord]: https://discord.gg/pallets
## Example
Signal receivers can subscribe to specific senders or receive signals
sent by any sender.
```pycon
>>> from blinker import signal
>>> started = signal('round-started')
>>> def each(round):
... print(f"Round {round}")
...
>>> started.connect(each)
>>> def round_two(round):
... print("This is round two.")
...
>>> started.connect(round_two, sender=2)
>>> for round in range(1, 4):
... started.send(round)
...
Round 1!
Round 2!
This is round two.
Round 3!
```

View File

@@ -1,12 +0,0 @@
blinker-1.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
blinker-1.9.0.dist-info/LICENSE.txt,sha256=nrc6HzhZekqhcCXSrhvjg5Ykx5XphdTw6Xac4p-spGc,1054
blinker-1.9.0.dist-info/METADATA,sha256=uIRiM8wjjbHkCtbCyTvctU37IAZk0kEe5kxAld1dvzA,1633
blinker-1.9.0.dist-info/RECORD,,
blinker-1.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82
blinker/__init__.py,sha256=I2EdZqpy4LyjX17Hn1yzJGWCjeLaVaPzsMgHkLfj_cQ,317
blinker/__pycache__/__init__.cpython-311.pyc,,
blinker/__pycache__/_utilities.cpython-311.pyc,,
blinker/__pycache__/base.cpython-311.pyc,,
blinker/_utilities.py,sha256=0J7eeXXTUx0Ivf8asfpx0ycVkp0Eqfqnj117x2mYX9E,1675
blinker/base.py,sha256=QpDuvXXcwJF49lUBcH5BiST46Rz9wSG7VW_p7N_027M,19132
blinker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0

View File

@@ -1,4 +0,0 @@
Wheel-Version: 1.0
Generator: flit 3.10.1
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -1,17 +0,0 @@
from __future__ import annotations
from .base import ANY
from .base import default_namespace
from .base import NamedSignal
from .base import Namespace
from .base import Signal
from .base import signal
__all__ = [
"ANY",
"default_namespace",
"NamedSignal",
"Namespace",
"Signal",
"signal",
]

View File

@@ -1,64 +0,0 @@
from __future__ import annotations
import collections.abc as c
import inspect
import typing as t
from weakref import ref
from weakref import WeakMethod
T = t.TypeVar("T")
class Symbol:
"""A constant symbol, nicer than ``object()``. Repeated calls return the
same instance.
>>> Symbol('foo') is Symbol('foo')
True
>>> Symbol('foo')
foo
"""
symbols: t.ClassVar[dict[str, Symbol]] = {}
def __new__(cls, name: str) -> Symbol:
if name in cls.symbols:
return cls.symbols[name]
obj = super().__new__(cls)
cls.symbols[name] = obj
return obj
def __init__(self, name: str) -> None:
self.name = name
def __repr__(self) -> str:
return self.name
def __getnewargs__(self) -> tuple[t.Any, ...]:
return (self.name,)
def make_id(obj: object) -> c.Hashable:
"""Get a stable identifier for a receiver or sender, to be used as a dict
key or in a set.
"""
if inspect.ismethod(obj):
# The id of a bound method is not stable, but the id of the unbound
# function and instance are.
return id(obj.__func__), id(obj.__self__)
if isinstance(obj, (str, int)):
# Instances with the same value always compare equal and have the same
# hash, even if the id may change.
return obj
# Assume other types are not hashable but will always be the same instance.
return id(obj)
def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]:
if inspect.ismethod(obj):
return WeakMethod(obj, callback) # type: ignore[arg-type, return-value]
return ref(obj, callback)

View File

@@ -1,512 +0,0 @@
from __future__ import annotations
import collections.abc as c
import sys
import typing as t
import weakref
from collections import defaultdict
from contextlib import contextmanager
from functools import cached_property
from inspect import iscoroutinefunction
from ._utilities import make_id
from ._utilities import make_ref
from ._utilities import Symbol
F = t.TypeVar("F", bound=c.Callable[..., t.Any])
ANY = Symbol("ANY")
"""Symbol for "any sender"."""
ANY_ID = 0
class Signal:
"""A notification emitter.
:param doc: The docstring for the signal.
"""
ANY = ANY
"""An alias for the :data:`~blinker.ANY` sender symbol."""
set_class: type[set[t.Any]] = set
"""The set class to use for tracking connected receivers and senders.
Python's ``set`` is unordered. If receivers must be dispatched in the order
they were connected, an ordered set implementation can be used.
.. versionadded:: 1.7
"""
@cached_property
def receiver_connected(self) -> Signal:
"""Emitted at the end of each :meth:`connect` call.
The signal sender is the signal instance, and the :meth:`connect`
arguments are passed through: ``receiver``, ``sender``, and ``weak``.
.. versionadded:: 1.2
"""
return Signal(doc="Emitted after a receiver connects.")
@cached_property
def receiver_disconnected(self) -> Signal:
"""Emitted at the end of each :meth:`disconnect` call.
The sender is the signal instance, and the :meth:`disconnect` arguments
are passed through: ``receiver`` and ``sender``.
This signal is emitted **only** when :meth:`disconnect` is called
explicitly. This signal cannot be emitted by an automatic disconnect
when a weakly referenced receiver or sender goes out of scope, as the
instance is no longer be available to be used as the sender for this
signal.
An alternative approach is available by subscribing to
:attr:`receiver_connected` and setting up a custom weakref cleanup
callback on weak receivers and senders.
.. versionadded:: 1.2
"""
return Signal(doc="Emitted after a receiver disconnects.")
def __init__(self, doc: str | None = None) -> None:
if doc:
self.__doc__ = doc
self.receivers: dict[
t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any]
] = {}
"""The map of connected receivers. Useful to quickly check if any
receivers are connected to the signal: ``if s.receivers:``. The
structure and data is not part of the public API, but checking its
boolean value is.
"""
self.is_muted: bool = False
self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class)
self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {}
def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F:
"""Connect ``receiver`` to be called when the signal is sent by
``sender``.
:param receiver: The callable to call when :meth:`send` is called with
the given ``sender``, passing ``sender`` as a positional argument
along with any extra keyword arguments.
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
called when :meth:`send` is called with this sender. If ``ANY``, the
receiver will be called for any sender. A receiver may be connected
to multiple senders by calling :meth:`connect` multiple times.
:param weak: Track the receiver with a :mod:`weakref`. The receiver will
be automatically disconnected when it is garbage collected. When
connecting a receiver defined within a function, set to ``False``,
otherwise it will be disconnected when the function scope ends.
"""
receiver_id = make_id(receiver)
sender_id = ANY_ID if sender is ANY else make_id(sender)
if weak:
self.receivers[receiver_id] = make_ref(
receiver, self._make_cleanup_receiver(receiver_id)
)
else:
self.receivers[receiver_id] = receiver
self._by_sender[sender_id].add(receiver_id)
self._by_receiver[receiver_id].add(sender_id)
if sender is not ANY and sender_id not in self._weak_senders:
# store a cleanup for weakref-able senders
try:
self._weak_senders[sender_id] = make_ref(
sender, self._make_cleanup_sender(sender_id)
)
except TypeError:
pass
if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers:
try:
self.receiver_connected.send(
self, receiver=receiver, sender=sender, weak=weak
)
except TypeError:
# TODO no explanation or test for this
self.disconnect(receiver, sender)
raise
return receiver
def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]:
"""Connect the decorated function to be called when the signal is sent
by ``sender``.
The decorated function will be called when :meth:`send` is called with
the given ``sender``, passing ``sender`` as a positional argument along
with any extra keyword arguments.
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
called when :meth:`send` is called with this sender. If ``ANY``, the
receiver will be called for any sender. A receiver may be connected
to multiple senders by calling :meth:`connect` multiple times.
:param weak: Track the receiver with a :mod:`weakref`. The receiver will
be automatically disconnected when it is garbage collected. When
connecting a receiver defined within a function, set to ``False``,
otherwise it will be disconnected when the function scope ends.=
.. versionadded:: 1.1
"""
def decorator(fn: F) -> F:
self.connect(fn, sender, weak)
return fn
return decorator
@contextmanager
def connected_to(
self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY
) -> c.Generator[None, None, None]:
"""A context manager that temporarily connects ``receiver`` to the
signal while a ``with`` block executes. When the block exits, the
receiver is disconnected. Useful for tests.
:param receiver: The callable to call when :meth:`send` is called with
the given ``sender``, passing ``sender`` as a positional argument
along with any extra keyword arguments.
:param sender: Any object or :data:`ANY`. ``receiver`` will only be
called when :meth:`send` is called with this sender. If ``ANY``, the
receiver will be called for any sender.
.. versionadded:: 1.1
"""
self.connect(receiver, sender=sender, weak=False)
try:
yield None
finally:
self.disconnect(receiver)
@contextmanager
def muted(self) -> c.Generator[None, None, None]:
"""A context manager that temporarily disables the signal. No receivers
will be called if the signal is sent, until the ``with`` block exits.
Useful for tests.
"""
self.is_muted = True
try:
yield None
finally:
self.is_muted = False
def send(
self,
sender: t.Any | None = None,
/,
*,
_async_wrapper: c.Callable[
[c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any]
]
| None = None,
**kwargs: t.Any,
) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
"""Call all receivers that are connected to the given ``sender``
or :data:`ANY`. Each receiver is called with ``sender`` as a positional
argument along with any extra keyword arguments. Return a list of
``(receiver, return value)`` tuples.
The order receivers are called is undefined, but can be influenced by
setting :attr:`set_class`.
If a receiver raises an exception, that exception will propagate up.
This makes debugging straightforward, with an assumption that correctly
implemented receivers will not raise.
:param sender: Call receivers connected to this sender, in addition to
those connected to :data:`ANY`.
:param _async_wrapper: Will be called on any receivers that are async
coroutines to turn them into sync callables. For example, could run
the receiver with an event loop.
:param kwargs: Extra keyword arguments to pass to each receiver.
.. versionchanged:: 1.7
Added the ``_async_wrapper`` argument.
"""
if self.is_muted:
return []
results = []
for receiver in self.receivers_for(sender):
if iscoroutinefunction(receiver):
if _async_wrapper is None:
raise RuntimeError("Cannot send to a coroutine function.")
result = _async_wrapper(receiver)(sender, **kwargs)
else:
result = receiver(sender, **kwargs)
results.append((receiver, result))
return results
async def send_async(
self,
sender: t.Any | None = None,
/,
*,
_sync_wrapper: c.Callable[
[c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]
]
| None = None,
**kwargs: t.Any,
) -> list[tuple[c.Callable[..., t.Any], t.Any]]:
"""Await all receivers that are connected to the given ``sender``
or :data:`ANY`. Each receiver is called with ``sender`` as a positional
argument along with any extra keyword arguments. Return a list of
``(receiver, return value)`` tuples.
The order receivers are called is undefined, but can be influenced by
setting :attr:`set_class`.
If a receiver raises an exception, that exception will propagate up.
This makes debugging straightforward, with an assumption that correctly
implemented receivers will not raise.
:param sender: Call receivers connected to this sender, in addition to
those connected to :data:`ANY`.
:param _sync_wrapper: Will be called on any receivers that are sync
callables to turn them into async coroutines. For example,
could call the receiver in a thread.
:param kwargs: Extra keyword arguments to pass to each receiver.
.. versionadded:: 1.7
"""
if self.is_muted:
return []
results = []
for receiver in self.receivers_for(sender):
if not iscoroutinefunction(receiver):
if _sync_wrapper is None:
raise RuntimeError("Cannot send to a non-coroutine function.")
result = await _sync_wrapper(receiver)(sender, **kwargs)
else:
result = await receiver(sender, **kwargs)
results.append((receiver, result))
return results
def has_receivers_for(self, sender: t.Any) -> bool:
"""Check if there is at least one receiver that will be called with the
given ``sender``. A receiver connected to :data:`ANY` will always be
called, regardless of sender. Does not check if weakly referenced
receivers are still live. See :meth:`receivers_for` for a stronger
search.
:param sender: Check for receivers connected to this sender, in addition
to those connected to :data:`ANY`.
"""
if not self.receivers:
return False
if self._by_sender[ANY_ID]:
return True
if sender is ANY:
return False
return make_id(sender) in self._by_sender
def receivers_for(
self, sender: t.Any
) -> c.Generator[c.Callable[..., t.Any], None, None]:
"""Yield each receiver to be called for ``sender``, in addition to those
to be called for :data:`ANY`. Weakly referenced receivers that are not
live will be disconnected and skipped.
:param sender: Yield receivers connected to this sender, in addition
to those connected to :data:`ANY`.
"""
# TODO: test receivers_for(ANY)
if not self.receivers:
return
sender_id = make_id(sender)
if sender_id in self._by_sender:
ids = self._by_sender[ANY_ID] | self._by_sender[sender_id]
else:
ids = self._by_sender[ANY_ID].copy()
for receiver_id in ids:
receiver = self.receivers.get(receiver_id)
if receiver is None:
continue
if isinstance(receiver, weakref.ref):
strong = receiver()
if strong is None:
self._disconnect(receiver_id, ANY_ID)
continue
yield strong
else:
yield receiver
def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None:
"""Disconnect ``receiver`` from being called when the signal is sent by
``sender``.
:param receiver: A connected receiver callable.
:param sender: Disconnect from only this sender. By default, disconnect
from all senders.
"""
sender_id: c.Hashable
if sender is ANY:
sender_id = ANY_ID
else:
sender_id = make_id(sender)
receiver_id = make_id(receiver)
self._disconnect(receiver_id, sender_id)
if (
"receiver_disconnected" in self.__dict__
and self.receiver_disconnected.receivers
):
self.receiver_disconnected.send(self, receiver=receiver, sender=sender)
def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None:
if sender_id == ANY_ID:
if self._by_receiver.pop(receiver_id, None) is not None:
for bucket in self._by_sender.values():
bucket.discard(receiver_id)
self.receivers.pop(receiver_id, None)
else:
self._by_sender[sender_id].discard(receiver_id)
self._by_receiver[receiver_id].discard(sender_id)
def _make_cleanup_receiver(
self, receiver_id: c.Hashable
) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]:
"""Create a callback function to disconnect a weakly referenced
receiver when it is garbage collected.
"""
def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None:
# If the interpreter is shutting down, disconnecting can result in a
# weird ignored exception. Don't call it in that case.
if not sys.is_finalizing():
self._disconnect(receiver_id, ANY_ID)
return cleanup
def _make_cleanup_sender(
self, sender_id: c.Hashable
) -> c.Callable[[weakref.ref[t.Any]], None]:
"""Create a callback function to disconnect all receivers for a weakly
referenced sender when it is garbage collected.
"""
assert sender_id != ANY_ID
def cleanup(ref: weakref.ref[t.Any]) -> None:
self._weak_senders.pop(sender_id, None)
for receiver_id in self._by_sender.pop(sender_id, ()):
self._by_receiver[receiver_id].discard(sender_id)
return cleanup
def _cleanup_bookkeeping(self) -> None:
"""Prune unused sender/receiver bookkeeping. Not threadsafe.
Connecting & disconnecting leaves behind a small amount of bookkeeping
data. Typical workloads using Blinker, for example in most web apps,
Flask, CLI scripts, etc., are not adversely affected by this
bookkeeping.
With a long-running process performing dynamic signal routing with high
volume, e.g. connecting to function closures, senders are all unique
object instances. Doing all of this over and over may cause memory usage
to grow due to extraneous bookkeeping. (An empty ``set`` for each stale
sender/receiver pair.)
This method will prune that bookkeeping away, with the caveat that such
pruning is not threadsafe. The risk is that cleanup of a fully
disconnected receiver/sender pair occurs while another thread is
connecting that same pair. If you are in the highly dynamic, unique
receiver/sender situation that has lead you to this method, that failure
mode is perhaps not a big deal for you.
"""
for mapping in (self._by_sender, self._by_receiver):
for ident, bucket in list(mapping.items()):
if not bucket:
mapping.pop(ident, None)
def _clear_state(self) -> None:
"""Disconnect all receivers and senders. Useful for tests."""
self._weak_senders.clear()
self.receivers.clear()
self._by_sender.clear()
self._by_receiver.clear()
class NamedSignal(Signal):
"""A named generic notification emitter. The name is not used by the signal
itself, but matches the key in the :class:`Namespace` that it belongs to.
:param name: The name of the signal within the namespace.
:param doc: The docstring for the signal.
"""
def __init__(self, name: str, doc: str | None = None) -> None:
super().__init__(doc)
#: The name of this signal.
self.name: str = name
def __repr__(self) -> str:
base = super().__repr__()
return f"{base[:-1]}; {self.name!r}>" # noqa: E702
class Namespace(dict[str, NamedSignal]):
"""A dict mapping names to signals."""
def signal(self, name: str, doc: str | None = None) -> NamedSignal:
"""Return the :class:`NamedSignal` for the given ``name``, creating it
if required. Repeated calls with the same name return the same signal.
:param name: The name of the signal.
:param doc: The docstring of the signal.
"""
if name not in self:
self[name] = NamedSignal(name, doc)
return self[name]
class _PNamespaceSignal(t.Protocol):
def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ...
default_namespace: Namespace = Namespace()
"""A default :class:`Namespace` for creating named signals. :func:`signal`
creates a :class:`NamedSignal` in this namespace.
"""
signal: _PNamespaceSignal = default_namespace.signal
"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given
``name``, creating it if required. Repeated calls with the same name return the
same signal.
"""

View File

@@ -1,84 +0,0 @@
Metadata-Version: 2.4
Name: click
Version: 8.3.1
Summary: Composable command line interface toolkit
Maintainer-email: Pallets <contact@palletsprojects.com>
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-Expression: BSD-3-Clause
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Typing :: Typed
License-File: LICENSE.txt
Requires-Dist: colorama; platform_system == 'Windows'
Project-URL: Changes, https://click.palletsprojects.com/page/changes/
Project-URL: Chat, https://discord.gg/pallets
Project-URL: Documentation, https://click.palletsprojects.com/
Project-URL: Donate, https://palletsprojects.com/donate
Project-URL: Source, https://github.com/pallets/click/
<div align="center"><img src="https://raw.githubusercontent.com/pallets/click/refs/heads/stable/docs/_static/click-name.svg" alt="" height="150"></div>
# Click
Click is a Python package for creating beautiful command line interfaces
in a composable way with as little code as necessary. It's the "Command
Line Interface Creation Kit". It's highly configurable but comes with
sensible defaults out of the box.
It aims to make the process of writing command line tools quick and fun
while also preventing any frustration caused by the inability to
implement an intended CLI API.
Click in three points:
- Arbitrary nesting of commands
- Automatic help page generation
- Supports lazy loading of subcommands at runtime
## A Simple Example
```python
import click
@click.command()
@click.option("--count", default=1, help="Number of greetings.")
@click.option("--name", prompt="Your name", help="The person to greet.")
def hello(count, name):
"""Simple program that greets NAME for a total of COUNT times."""
for _ in range(count):
click.echo(f"Hello, {name}!")
if __name__ == '__main__':
hello()
```
```
$ python hello.py --count=3
Your name: Click
Hello, Click!
Hello, Click!
Hello, Click!
```
## Donate
The Pallets organization develops and supports Click and other popular
packages. In order to grow the community of contributors and users, and
allow the maintainers to devote more time to the projects, [please
donate today][].
[please donate today]: https://palletsprojects.com/donate
## Contributing
See our [detailed contributing documentation][contrib] for many ways to
contribute, including reporting issues, requesting features, asking or answering
questions, and making PRs.
[contrib]: https://palletsprojects.com/contributing/

View File

@@ -1,40 +0,0 @@
click-8.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
click-8.3.1.dist-info/METADATA,sha256=XZeBrMAE0ghTE88SjfrSDuSyNCpBPplxJR1tbwD9oZg,2621
click-8.3.1.dist-info/RECORD,,
click-8.3.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
click-8.3.1.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475
click/__init__.py,sha256=6YyS1aeyknZ0LYweWozNZy0A9nZ_11wmYIhv3cbQrYo,4473
click/__pycache__/__init__.cpython-311.pyc,,
click/__pycache__/_compat.cpython-311.pyc,,
click/__pycache__/_termui_impl.cpython-311.pyc,,
click/__pycache__/_textwrap.cpython-311.pyc,,
click/__pycache__/_utils.cpython-311.pyc,,
click/__pycache__/_winconsole.cpython-311.pyc,,
click/__pycache__/core.cpython-311.pyc,,
click/__pycache__/decorators.cpython-311.pyc,,
click/__pycache__/exceptions.cpython-311.pyc,,
click/__pycache__/formatting.cpython-311.pyc,,
click/__pycache__/globals.cpython-311.pyc,,
click/__pycache__/parser.cpython-311.pyc,,
click/__pycache__/shell_completion.cpython-311.pyc,,
click/__pycache__/termui.cpython-311.pyc,,
click/__pycache__/testing.cpython-311.pyc,,
click/__pycache__/types.cpython-311.pyc,,
click/__pycache__/utils.cpython-311.pyc,,
click/_compat.py,sha256=v3xBZkFbvA1BXPRkFfBJc6-pIwPI7345m-kQEnpVAs4,18693
click/_termui_impl.py,sha256=rgCb3On8X5A4200rA5L6i13u5iapmFer7sru57Jy6zA,27093
click/_textwrap.py,sha256=BOae0RQ6vg3FkNgSJyOoGzG1meGMxJ_ukWVZKx_v-0o,1400
click/_utils.py,sha256=kZwtTf5gMuCilJJceS2iTCvRvCY-0aN5rJq8gKw7p8g,943
click/_winconsole.py,sha256=_vxUuUaxwBhoR0vUWCNuHY8VUefiMdCIyU2SXPqoF-A,8465
click/core.py,sha256=U6Bfxt8GkjNDqyJ0HqXvluJHtyZ4sY5USAvM1Cdq7mQ,132105
click/decorators.py,sha256=5P7abhJtAQYp_KHgjUvhMv464ERwOzrv2enNknlwHyQ,18461
click/exceptions.py,sha256=8utf8w6V5hJXMnO_ic1FNrtbwuEn1NUu1aDwV8UqnG4,9954
click/formatting.py,sha256=RVfwwr0rwWNpgGr8NaHodPzkIr7_tUyVh_nDdanLMNc,9730
click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923
click/parser.py,sha256=Q31pH0FlQZEq-UXE_ABRzlygEfvxPTuZbWNh4xfXmzw,19010
click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
click/shell_completion.py,sha256=Cc4GQUFuWpfQBa9sF5qXeeYI7n3tI_1k6ZdSn4BZbT0,20994
click/termui.py,sha256=hqCEjNndU-nzW08nRAkBaVgfZp_FdCA9KxfIWlKYaMc,31037
click/testing.py,sha256=EERbzcl1br0mW0qBS9EqkknfNfXB9WQEW0ELIpkvuSs,19102
click/types.py,sha256=ek54BNSFwPKsqtfT7jsqcc4WHui8AIFVMKM4oVZIXhc,39927
click/utils.py,sha256=gCUoewdAhA-QLBUUHxrLh4uj6m7T1WjZZMNPvR0I7YA,20257

View File

@@ -1,4 +0,0 @@
Wheel-Version: 1.0
Generator: flit 3.12.0
Root-Is-Purelib: true
Tag: py3-none-any

View File

@@ -1,28 +0,0 @@
Copyright 2014 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,123 +0,0 @@
"""
Click is a simple Python module inspired by the stdlib optparse to make
writing command line scripts fun. Unlike other modules, it's based
around a simple API that does not come with too much magic and is
composable.
"""
from __future__ import annotations
from .core import Argument as Argument
from .core import Command as Command
from .core import CommandCollection as CommandCollection
from .core import Context as Context
from .core import Group as Group
from .core import Option as Option
from .core import Parameter as Parameter
from .decorators import argument as argument
from .decorators import command as command
from .decorators import confirmation_option as confirmation_option
from .decorators import group as group
from .decorators import help_option as help_option
from .decorators import make_pass_decorator as make_pass_decorator
from .decorators import option as option
from .decorators import pass_context as pass_context
from .decorators import pass_obj as pass_obj
from .decorators import password_option as password_option
from .decorators import version_option as version_option
from .exceptions import Abort as Abort
from .exceptions import BadArgumentUsage as BadArgumentUsage
from .exceptions import BadOptionUsage as BadOptionUsage
from .exceptions import BadParameter as BadParameter
from .exceptions import ClickException as ClickException
from .exceptions import FileError as FileError
from .exceptions import MissingParameter as MissingParameter
from .exceptions import NoSuchOption as NoSuchOption
from .exceptions import UsageError as UsageError
from .formatting import HelpFormatter as HelpFormatter
from .formatting import wrap_text as wrap_text
from .globals import get_current_context as get_current_context
from .termui import clear as clear
from .termui import confirm as confirm
from .termui import echo_via_pager as echo_via_pager
from .termui import edit as edit
from .termui import getchar as getchar
from .termui import launch as launch
from .termui import pause as pause
from .termui import progressbar as progressbar
from .termui import prompt as prompt
from .termui import secho as secho
from .termui import style as style
from .termui import unstyle as unstyle
from .types import BOOL as BOOL
from .types import Choice as Choice
from .types import DateTime as DateTime
from .types import File as File
from .types import FLOAT as FLOAT
from .types import FloatRange as FloatRange
from .types import INT as INT
from .types import IntRange as IntRange
from .types import ParamType as ParamType
from .types import Path as Path
from .types import STRING as STRING
from .types import Tuple as Tuple
from .types import UNPROCESSED as UNPROCESSED
from .types import UUID as UUID
from .utils import echo as echo
from .utils import format_filename as format_filename
from .utils import get_app_dir as get_app_dir
from .utils import get_binary_stream as get_binary_stream
from .utils import get_text_stream as get_text_stream
from .utils import open_file as open_file
def __getattr__(name: str) -> object:
import warnings
if name == "BaseCommand":
from .core import _BaseCommand
warnings.warn(
"'BaseCommand' is deprecated and will be removed in Click 9.0. Use"
" 'Command' instead.",
DeprecationWarning,
stacklevel=2,
)
return _BaseCommand
if name == "MultiCommand":
from .core import _MultiCommand
warnings.warn(
"'MultiCommand' is deprecated and will be removed in Click 9.0. Use"
" 'Group' instead.",
DeprecationWarning,
stacklevel=2,
)
return _MultiCommand
if name == "OptionParser":
from .parser import _OptionParser
warnings.warn(
"'OptionParser' is deprecated and will be removed in Click 9.0. The"
" old parser is available in 'optparse'.",
DeprecationWarning,
stacklevel=2,
)
return _OptionParser
if name == "__version__":
import importlib.metadata
import warnings
warnings.warn(
"The '__version__' attribute is deprecated and will be removed in"
" Click 9.1. Use feature detection or"
" 'importlib.metadata.version(\"click\")' instead.",
DeprecationWarning,
stacklevel=2,
)
return importlib.metadata.version("click")
raise AttributeError(name)

Some files were not shown because too many files have changed in this diff Show More