Compare commits
58 Commits
d132da6923
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 865fde00c2 | |||
| f275ad60f0 | |||
| 9770617cd4 | |||
| a14ebc3243 | |||
| a30b9a9ba2 | |||
| 01bf68a77f | |||
| fe093d6c9d | |||
| 23bc302168 | |||
| 1a93d18369 | |||
| a8f236427e | |||
|
|
825f153f51 | ||
|
|
9977ce7797 | ||
|
|
cd9d837135 | ||
| 902a626776 | |||
| fea6467396 | |||
| 23f7f78178 | |||
| 0ea3e846f1 | |||
| 41312b0f75 | |||
| 0ab04110ad | |||
| cc0365a2e1 | |||
| 904d5e3475 | |||
| 9c33d832b7 | |||
| e7a55b4788 | |||
|
|
b7ef783790 | ||
| 0dfc9356fe | |||
| 4f2d3018b5 | |||
| 059552eb81 | |||
| fa94a4564d | |||
| 6d8f7dad4a | |||
| 7790694fb9 | |||
| af8e674cb9 | |||
| bf5b24214c | |||
| c378be1695 | |||
| 98aa5fceb1 | |||
| 488289e2cf | |||
| 76e8ecadc1 | |||
| bb566f7da2 | |||
| 703f7df12f | |||
| dbf6e807ae | |||
| fd17c22dfc | |||
| 5d6a2bb32b | |||
| 72a86ff512 | |||
| 8c3713eff6 | |||
| 785508ba53 | |||
| 0d3f9d82d4 | |||
| 951f073481 | |||
| 9a109c49e6 | |||
| 4e7a41d7aa | |||
| 119027dc00 | |||
| 87774e5303 | |||
| 0db365014a | |||
| 1eee629102 | |||
| 474a10aa43 | |||
| 50a48c5292 | |||
| afee1262eb | |||
| 9d3c201672 | |||
| f5ba39da8d | |||
| 1f84b5bc09 |
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
*.pem
|
||||||
|
.env
|
||||||
|
venv
|
||||||
15
LICENSE
Normal file
15
LICENSE
Normal 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
|
||||||
26
README.md
26
README.md
@@ -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 l’automatisation reste incomplète et l’arborescence des deux Raspberry Pi n’est pas identique. Des améliorations doivent encore être apportées aux scripts afin d’assurer une automatisation complète. Il est également possible que certaines informations du README soient manquantes ou ne soient plus en adéquation avec l’arborescence, 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**
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
|
|
||||||
10
composants/byPanda/DHT11.py
Normal file
10
composants/byPanda/DHT11.py
Normal 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
12
composants/byPanda/LDR.py
Normal 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"
|
||||||
BIN
composants/byPanda/__pycache__/alarme.cpython-311.pyc
Normal file
BIN
composants/byPanda/__pycache__/alarme.cpython-311.pyc
Normal file
Binary file not shown.
186
composants/byPanda/alarme.py
Normal file
186
composants/byPanda/alarme.py
Normal 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.")
|
||||||
|
|
||||||
246
composants/byPanda/alarme_test.py
Normal file
246
composants/byPanda/alarme_test.py
Normal 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)
|
||||||
24
composants/byPanda/board1main.py
Normal file
24
composants/byPanda/board1main.py
Normal 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()
|
||||||
32
composants/byPanda/board2main.py
Normal file
32
composants/byPanda/board2main.py
Normal 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()
|
||||||
46
composants/byPanda/bouton.py
Normal file
46
composants/byPanda/bouton.py
Normal 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)
|
||||||
60
composants/byPanda/bouton_servo.py
Normal file
60
composants/byPanda/bouton_servo.py
Normal 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()
|
||||||
25
composants/byPanda/etatsysteme.py
Normal file
25
composants/byPanda/etatsysteme.py
Normal 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)
|
||||||
15
composants/byPanda/ledRGB.py
Normal file
15
composants/byPanda/ledRGB.py
Normal 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)
|
||||||
87
composants/byPanda/lumieres.py
Normal file
87
composants/byPanda/lumieres.py
Normal 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()
|
||||||
91
composants/byPanda/porterfid.py
Normal file
91
composants/byPanda/porterfid.py
Normal 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)
|
||||||
32
composants/byPanda/septsegments.py
Normal file
32
composants/byPanda/septsegments.py
Normal 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}")
|
||||||
76
composants/byPanda/thermostat.py
Normal file
76
composants/byPanda/thermostat.py
Normal 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
|
||||||
73
composants/byPanda/volets.py
Normal file
73
composants/byPanda/volets.py
Normal 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()
|
||||||
@@ -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)
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
205
composants/test/ALARM_V2.py
Normal 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
15
composants/test/PIR.py
Normal 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)
|
||||||
36
composants/test/ledPorte.py
Normal file
36
composants/test/ledPorte.py
Normal 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
15
composants/test/ledRGB.py
Normal 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
15
composants/test/rfid.py
Normal 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
19
composants/test/servo.py
Normal 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
79
fastapi/README.md
Normal 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
106
fastapi/main.py
Normal 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
183
fastapi/main.sh
Executable 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
5
fastapi/requirement.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi
|
||||||
|
uvicorn
|
||||||
|
rpi.gpio
|
||||||
|
python-tm1637
|
||||||
|
Adafruit_DHT
|
||||||
69
fastapi/run_api.sh
Executable file
69
fastapi/run_api.sh
Executable 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
6
flask/.env
Normal 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
|
||||||
@@ -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
219
flask/index.html
Normal 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>
|
||||||
22
flask/led.py
22
flask/led.py
@@ -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()
|
|
||||||
"""
|
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)"
|
||||||
@@ -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
|
||||||
@@ -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');
|
||||||
|
|||||||
@@ -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";
|
||||||
}
|
}
|
||||||
|
|
||||||
function go_admin (){
|
try {
|
||||||
window.location.href = "/admin";
|
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);
|
||||||
|
updateAll();
|
||||||
|
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;
|
||||||
|
|||||||
@@ -3,177 +3,140 @@
|
|||||||
<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>
|
||||||
@@ -187,16 +150,17 @@
|
|||||||
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 {
|
||||||
@@ -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";
|
||||||
|
msg.textContent = "Connexion réussie !";
|
||||||
window.location.href = "/dashboard";
|
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 () => {
|
||||||
|
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 msg = document.getElementById("msg");
|
||||||
const path = document.getElementById("icon-path");
|
const btn = document.getElementById("btn");
|
||||||
document.getElementById("msg-text").textContent = text;
|
const inputs = document.querySelectorAll("input");
|
||||||
msg.className = "message " + type;
|
|
||||||
path.setAttribute("d", type === "success"
|
btn.disabled = true;
|
||||||
? "M20 6L9 17l-5-5"
|
inputs.forEach(input => input.disabled = true);
|
||||||
: "M12 2a10 10 0 1 0 0 20A10 10 0 0 0 12 2zm0 6v4m0 4h.01"
|
|
||||||
);
|
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>
|
||||||
|
|||||||
@@ -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
16
to_do.txt
Normal 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
|
||||||
@@ -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"
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -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())
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
python3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/usr/bin/python3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
python3
|
|
||||||
@@ -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
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
__import__('_distutils_hack').do_override()
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pip
|
|
||||||
@@ -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 doesn’t 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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
|
|
||||||
@@ -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.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
bcrypt
|
|
||||||
@@ -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",
|
|
||||||
]
|
|
||||||
@@ -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: ...
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1 +0,0 @@
|
|||||||
pip
|
|
||||||
@@ -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.
|
|
||||||
@@ -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!
|
|
||||||
```
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: flit 3.10.1
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
||||||
@@ -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",
|
|
||||||
]
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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)
|
|
||||||
@@ -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.
|
|
||||||
"""
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
pip
|
|
||||||
@@ -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/
|
|
||||||
|
|
||||||
@@ -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
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
Wheel-Version: 1.0
|
|
||||||
Generator: flit 3.12.0
|
|
||||||
Root-Is-Purelib: true
|
|
||||||
Tag: py3-none-any
|
|
||||||
@@ -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.
|
|
||||||
@@ -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)
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user