Import from Gitlab
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
THINGSBOARD_URL=
|
||||
|
||||
THINGSBOARD_USER_SITE=
|
||||
|
||||
DB_HOST=
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
DB_PORT=
|
||||
DB_DATABASE=
|
||||
|
||||
#telemetry or devices or history
|
||||
SAVE_WHAT=
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
.idea/
|
||||
.venv/
|
||||
.env
|
||||
|
||||
storage/data/*
|
||||
!storage/data/output/
|
||||
storage/data/output/*
|
||||
!storage/data/output/.gitkeep
|
||||
|
||||
storage/logs/*
|
||||
!storage/logs/.gitkeep
|
||||
@@ -0,0 +1,138 @@
|
||||
|
||||
# Intégration capteurs
|
||||
## Introduction
|
||||
Ce projet possède trois modes :
|
||||
|
||||
- devices : permet d’enregistrer un ou plusieurs nouveaux capteurs (devices).
|
||||
- telemetry : permet d’enregistrer des données de télémétrie sur un capteur donné.
|
||||
- history : permet d'enregistrer des données de télémétrie issue du Data INRAE sur un capteur donné.
|
||||
|
||||
Dans les trois cas, les opérations sont effectuées avec le compte ThingsBoard dont le nom correspond à la valeur de la variable d’environnement THINGSBOARD_USER_SITE (Bresle, Oir ou Scorff).
|
||||
|
||||
Des fichiers de tests sont accessibles sur le dossier de partage `projets` :
|
||||
- pour les devices : `projets/BDPOISSON/Stage_Giovanni/Fichiers%20test/Appareils/`
|
||||
- pour les telemetry : `projets/BDPOISSON/Stage_Giovanni/Fichiers%20test/Temperatures/20230111-TEMPA-TEMP_BRE_01-BRESLE_Eu-21164191.txt`
|
||||
- pour les history : `projets/BDPOISSON/Stage_Giovanni/Fichiers%20test/Historiques/`
|
||||
|
||||
## Installation
|
||||
|
||||
### Cloner le dépôt
|
||||
Pour installer le projet sur votre serveur ou ordinateur, clonez le dépôt à l'endroit souhaitez :
|
||||
```bash
|
||||
git clone <adresse du dépôt>
|
||||
cd integration_capteurs
|
||||
```
|
||||
### Environnement virtuel
|
||||
À la racine du projet, créez et activez un environnement virtuel :
|
||||
```bash
|
||||
python3 -m venv .venv source .venv/bin/activate # Linux/Mac # .\.venv\Scripts\activate # Windows`
|
||||
```
|
||||
|
||||
Installez ensuite les dépendances :
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
### Variables d'environnements
|
||||
|
||||
1. Copiez le fichier `.env.example` en `.env` à la racine du projet.
|
||||
2. Remplissez les variables suivantes dans le fichier `.env` :
|
||||
|
||||
| Variables | Explications |
|
||||
| ----------------------- |---------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| `THINGSBOARD_URL` | URL de l'outil ThingsBoard, terminée par `/api/`. |
|
||||
| `THINGSBOARD_USER_SITE` | Nom du bassin versant de l'utilisateur ThingsBoard (Bresle, Scorff ou Oir) |
|
||||
| `DB_HOST` | Adresse du serveur de base de données. |
|
||||
| `DB_USERNAME` | Nom d'utilisateur pour la connection à la base de données. |
|
||||
| `DB_PASSWORD` | Mot de passe de cet utilisateur. |
|
||||
| `DB_PORT` | Port de la base de données. |
|
||||
| `DB_DATABASE` | Nom de la base de données à utiliser. |
|
||||
| `SAVE_WHAT` | Ce que vous souhaitez sauvegarder (`devices` pour un capteur, `telemetry` pour des données ou `history` pour des données historiques) |
|
||||
|
||||
## Conventions
|
||||
|
||||
### Fichier de données de télémétrie
|
||||
Le nom du fichier doit respecter le format suivant :
|
||||
date_du_releve-type_de_donnee-code_secteur-libelle_secteur-numero_serie.txt
|
||||
|
||||
Par exemple :
|
||||
20261025-TEMPA-TEMP_BRE_01-BRESLE_Eu-32268041.txt
|
||||
|
||||
Si vous devez ajouter des informations supplémentaires, placer-les à la fin du nom de fichier, après le numéro de série.
|
||||
|
||||
Pour plus de détails concernant les conventions de fichier de données, veuillez vous référer au guide utilisateur.
|
||||
|
||||
> Pour les fichiers de télémétrie, la première ligne du fichier doit contenir au moins un caractère avant la ligne listant les noms de colonnes. Cette ligne est utilisée par le script lors de l'import des données.
|
||||
|
||||
### Fichier de capteur
|
||||
Les fichiers de capteurs doivent respecter les contraintes suivantes afin dêtre correctement interprétés par le script :
|
||||
- Le séparateur de valeurs est le point-virgule ( ; ).
|
||||
- Le fichier peux être au format .csv ou .txt.
|
||||
- Le fichier doit contenir les colonnes suivantes dans le même ordre :
|
||||
|
||||
| Ordre | Nom | Descritpion | Optionnel |
|
||||
| ----- | -------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | --------- |
|
||||
| 1 | sector_code | Numéro du secteur | Non |
|
||||
| 2 | serial_number | Numéro de série du capteur | Non |
|
||||
| 3 | data_type | Type de données mesuré (voir le guide utilisateur pour prendre connaissance de la liste des type de données) | Non |
|
||||
| 4 | sector_wording | Libellé du secteur | Non |
|
||||
| 5 | description | Peux contenir ce que vous souhaitez comme par exemple le ou les projets/expérimentations associés au capteur avec les dates correspondantes. | Oui |
|
||||
|
||||
### Fichier de données historique
|
||||
Les fichiers de données historique doivent respecter les contraintes suivantes afin dêtre correctement interprétés par le script :
|
||||
- Nom de fichier au format : code_secteur-type_de_donnee.txt
|
||||
- Que toutes les données d'un fichier ne correspondent qu'à un seul et unique secteur.
|
||||
- Que la date et l'heure soit dans 2 colonnes différentes.
|
||||
|
||||
|
||||
## Lancement du projet
|
||||
|
||||
Si vous n'avez pas activé votre environnement virtuel :
|
||||
```bash
|
||||
source .venv/bin/activate # Linux/Mac # .\.venv\Scripts\activate # Windows`
|
||||
```
|
||||
|
||||
Tout en étant à la racine du projet, exécutez le script Python :
|
||||
```bash
|
||||
python3 main.py
|
||||
```
|
||||
|
||||
## Visualisation
|
||||
|
||||
### Visualiser des données de télémétrie
|
||||
Lorsque `SAVE_WHAT=telemetry`, le script envoie des données vers un capteur existant dans ThingsBoard. Cependant si le capteur n'existe pas encore sur ThingsBoard, veuillez commencer par le créer.
|
||||
|
||||
1. Connectez-vous à votre instance ThingsBoard.
|
||||
2. Accédez au menu `Devices`.
|
||||
3. Sélectionnez le capteur concerné (nom : `numéro_secteur-numéro_inventaire`).
|
||||
4. Ouvrez l’onglet `Latest telemetry`.
|
||||
|
||||
Vous y verrez les dernières valeurs envoyées par le script.
|
||||
|
||||
|
||||
Pour repartir de zéro :
|
||||
|
||||
1. Allez dans le device
|
||||
2. Ouvrez `Latest telemetry`.
|
||||
3. Supprimez toutes les entrées via l’icône de poubelle.
|
||||
4. Relancez le script pour vérifier que de nouvelles données apparaissent.
|
||||
|
||||
### Visualiser des capteurs
|
||||
Lorsque `SAVE_WHAT=devices`, le script crée automatiquement des capteurs dans ThingsBoard.
|
||||
|
||||
1. Connectez-vous à votre instance ThingsBoard.
|
||||
2. Accédez au menu `Devices`.
|
||||
3. Les nouveaux capteurs apparaissent automatiquement dans le groupe correspondant à l’utilisateur (`THINGSBOARD_USER_SITE`) avec un nom au format : `numéro_secteur-numéro_inventaire`
|
||||
|
||||
|
||||
### Visualiser des données historique
|
||||
Lorsque `SAVE_WHAT=history`, le script fonctionne presque de la même manière qu'en mode `telemetry`. Il envoie les données vers un capteur existant dans ThingsBoard. Cependant si le capteur n'existe pas encore sur ThingsBoard, veuillez commencer par le créer.
|
||||
|
||||
1. Connectez-vous à votre instance ThingsBoard.
|
||||
2. Accédez au menu `Devices`.
|
||||
3. Sélectionnez le capteur concerné (nom : `numéro_secteur-numéro_inventaire`).
|
||||
4. Ouvrez l’onglet `Latest telemetry`.
|
||||
|
||||
Vous y verrez les dernières valeurs envoyées par le script.
|
||||
@@ -0,0 +1,67 @@
|
||||
import json
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog
|
||||
from pathlib import Path
|
||||
|
||||
from app.config import config
|
||||
from app.services.dispatcher import dispatcher
|
||||
from app.utils.auth.login import login
|
||||
from app.utils.auth.logout import logout
|
||||
from app.utils.auth.get_current_user import get_current_user
|
||||
from app.utils.write_user_message import write_user_message
|
||||
|
||||
|
||||
# TODO : faire que l'on puisse ouvrir plusieurs fichiers de données et que l'erreur de l'un des fichiers n'arrete pas le script (comme pour les devices)
|
||||
|
||||
def init_app():
|
||||
write_user_message("Début du script en mode '" + str(config.save_what) + ".", "info")
|
||||
try:
|
||||
file_path = select_file_path()
|
||||
file_name = file_path.split("/")[-1]
|
||||
|
||||
user_id = authenticate_thingsboard_user()
|
||||
|
||||
dispatcher(file_path, file_name, user_id)
|
||||
|
||||
except Exception as e:
|
||||
write_user_message(str(e), "exception")
|
||||
|
||||
else:
|
||||
write_user_message("Script terminé avec succès !", "info")
|
||||
|
||||
|
||||
|
||||
|
||||
def select_file_path():
|
||||
write_user_message("En attente que l'utilisateur sélectionne un fichier...", "info")
|
||||
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="Choisissez un fichier",
|
||||
initialdir=Path.home(),
|
||||
filetypes=[(".csv ou .txt", "*.csv .txt")]
|
||||
)
|
||||
|
||||
root.destroy()
|
||||
|
||||
return file_path
|
||||
|
||||
|
||||
|
||||
def authenticate_thingsboard_user():
|
||||
user = get_current_user()
|
||||
|
||||
data = {
|
||||
"username": config.thingsboard_user[4],
|
||||
"password": config.thingsboard_user[5]
|
||||
}
|
||||
|
||||
if user is None or json.loads(user)["email"] != config.thingsboard_user[4]:
|
||||
if user is not None:
|
||||
logout()
|
||||
|
||||
login(data)
|
||||
user = get_current_user()
|
||||
|
||||
return json.loads(user)['customerId']['id']
|
||||
@@ -0,0 +1,32 @@
|
||||
import os
|
||||
import logging
|
||||
|
||||
from app.database.connection import init_connection
|
||||
from app.database.queries.get_thingsboard_user import get_thingsboard_user
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self):
|
||||
# Init environment variables
|
||||
self.save_what = os.getenv("SAVE_WHAT")
|
||||
self.thingsboard_url = os.getenv("THINGSBOARD_URL")
|
||||
self.thingsboard_user_site = os.getenv("THINGSBOARD_USER_SITE")
|
||||
|
||||
|
||||
# Init logger
|
||||
self.logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(
|
||||
filename='storage/logs/logs.log',
|
||||
encoding='utf-8',
|
||||
level=logging.DEBUG
|
||||
)
|
||||
|
||||
# Init database connection
|
||||
self.db_connection = init_connection(self.logger)
|
||||
|
||||
# Get ThingsBoard user
|
||||
self.thingsboard_user = get_thingsboard_user(os.getenv('THINGSBOARD_USER_SITE'), self.db_connection)
|
||||
|
||||
|
||||
|
||||
config = Config()
|
||||
@@ -0,0 +1,17 @@
|
||||
import datetime
|
||||
|
||||
import psycopg2
|
||||
import os
|
||||
|
||||
def init_connection(logger):
|
||||
now = datetime.datetime.now()
|
||||
print(str(now) + ":INFO:" + "Connexion à la base de données en cours...")
|
||||
logger.info(str(now) + ":INFO:" + "Connexion à la base de données en cours...")
|
||||
|
||||
return psycopg2.connect(
|
||||
database = os.getenv("DB_DATABASE"),
|
||||
user = os.getenv("DB_USER"),
|
||||
password = os.getenv("DB_PASSWORD"),
|
||||
host = os.getenv("DB_HOST"),
|
||||
port = os.getenv("DB_PORT")
|
||||
)
|
||||
@@ -0,0 +1,19 @@
|
||||
from app.config import config
|
||||
|
||||
|
||||
def get_device_inventory_number(serial_number):
|
||||
conn = config.db_connection
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = ("SELECT n_inventaire_u3e from materiel.t_materiel where num_serie = %s")
|
||||
cur.execute(sql, (str(serial_number),))
|
||||
rows = cur.fetchall()
|
||||
|
||||
cur.close()
|
||||
|
||||
if len(rows) == 0:
|
||||
raise Exception("Aucun appareil ne correspond à ce numéro de série dans la base matériel.")
|
||||
elif len(rows) > 1:
|
||||
raise Exception("Plusieurs appareils correspondent à ce numéro de série dans la base matériel.")
|
||||
|
||||
return rows[0][0]
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
|
||||
def get_thingsboard_user(user_lastname, conn):
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = ("SELECT * from public.tr_thingsboard_user_tbu where tbu_nom = %s")
|
||||
cur.execute(sql, (str(user_lastname),))
|
||||
rows = cur.fetchall()
|
||||
|
||||
cur.close()
|
||||
|
||||
if len(rows) == 0:
|
||||
raise Exception("Aucune clé d'API ThingsBoard ne correspond dans la base de données.")
|
||||
elif len(rows) > 1:
|
||||
raise Exception("Plusieurs clés d'API ThingsBoard correspondent dans la base de données.")
|
||||
|
||||
return rows[0]
|
||||
@@ -0,0 +1,18 @@
|
||||
from app import config
|
||||
|
||||
|
||||
def is_sector_code_existing(secteur_code):
|
||||
lowered_site_name = config.thingsboard_user_site.lower()
|
||||
conn = config.db_connection
|
||||
cur = conn.cursor()
|
||||
|
||||
sql = (f"SELECT sec_code from {lowered_site_name}.tr_secteur_sec where sec_code = %s")
|
||||
cur.execute(sql, (str(secteur_code),))
|
||||
rows = cur.fetchall()
|
||||
|
||||
cur.close()
|
||||
|
||||
if len(rows) == 0:
|
||||
raise Exception("Aucun code de secteur ne correspond dans la base " + lowered_site_name + ".tr_secteur_sec")
|
||||
elif len(rows) > 1:
|
||||
raise Exception("Plusieurs codes de secteurs correspondent dans la base " + lowered_site_name + ".tr_secteur_sec")
|
||||
@@ -0,0 +1,30 @@
|
||||
from app.config import config
|
||||
from app.services.save_devices.save_devices import save_devices
|
||||
from app.services.save_history.format_history import format_history
|
||||
from app.services.save_history.save_history import save_history
|
||||
from app.services.save_telemetry.format_telemetry import format_telemetry
|
||||
from app.services.save_telemetry.save_telemetry import save_telemetry
|
||||
from app.utils.write_user_message import write_user_message
|
||||
|
||||
|
||||
def dispatcher(file_path, file_name, user_id):
|
||||
if config.save_what == "telemetry":
|
||||
data_frame = format_telemetry(file_path, file_name)
|
||||
write_user_message("DataFrame formatted successfully !", "info")
|
||||
|
||||
save_telemetry(file_name, data_frame, user_id)
|
||||
write_user_message("Télémétrie enregistrées avec succès !", "info")
|
||||
|
||||
elif config.save_what == "devices":
|
||||
save_devices(file_path, user_id)
|
||||
|
||||
elif config.save_what == "history":
|
||||
data_frame = format_history(file_path)
|
||||
write_user_message("DataFrame formatted successfully !", "info")
|
||||
|
||||
save_history(file_name, data_frame, user_id)
|
||||
write_user_message("Télémétrie historiques enregistrées avec succès !", "info")
|
||||
|
||||
else:
|
||||
write_user_message(
|
||||
"Veuillez spécifier une valeur correct pour la variable d'environnement 'SAVE_WHAT' ('telemetry' ou 'devices' ou 'history').", "exception")
|
||||
@@ -0,0 +1,85 @@
|
||||
import json
|
||||
import uuid
|
||||
import pandas as pd
|
||||
from pandas import isna
|
||||
|
||||
from app.database.queries.get_device_inventory_number import get_device_inventory_number
|
||||
from app.database.queries.is_sector_code_existing import is_sector_code_existing
|
||||
from app.utils.devices.get_user_device import get_user_device
|
||||
from app.utils.devices.save_entity_attribute import save_entity_attribute
|
||||
from app.utils.devices.save_new_device import save_new_device
|
||||
from app.utils.write_user_message import write_user_message
|
||||
|
||||
|
||||
def save_devices(file_path, user_id):
|
||||
"""
|
||||
Saves devices dictionary to the ThingsBoard database.
|
||||
:param file_path: The path of the file to be read.
|
||||
:type file_path: String
|
||||
:param user_id: The id of the current ThingsBoard user
|
||||
:type user_id: String
|
||||
:return: No return
|
||||
"""
|
||||
write_user_message("Création des nouveaux appareils sur ThingsBoard en cours...", "info")
|
||||
|
||||
data_frame = pd.read_csv(file_path, sep=";")
|
||||
|
||||
if len(data_frame.columns) != 5:
|
||||
raise Exception("Le fichier ne contient pas le bon nombre de colonne (sector_code;serial_number;data_type;sector_wording;description).")
|
||||
|
||||
invalid_rows = data_frame[data_frame[["sector_code", "serial_number", "data_type", "sector_wording"]].isnull().any(axis=1)]
|
||||
if len(invalid_rows.index) > 0:
|
||||
raise Exception("Certaines lignes contiennent des données vides.")
|
||||
|
||||
|
||||
|
||||
|
||||
for device in data_frame.values:
|
||||
device_sector_code = device[0]
|
||||
|
||||
try:
|
||||
is_sector_code_existing(device_sector_code)
|
||||
|
||||
device_inventory_number = get_device_inventory_number(str(device[1]))
|
||||
potential_device_name = device[0] + "-" + device_inventory_number
|
||||
|
||||
devices = get_user_device(user_id, potential_device_name)
|
||||
jsoned_devices = json.loads(devices)
|
||||
|
||||
if len(jsoned_devices["data"]) > 0:
|
||||
raise Exception("Appareil déjà existant sur ThingsBoard.")
|
||||
|
||||
|
||||
description = ""
|
||||
if not isna(device[4]):
|
||||
description = device[4]
|
||||
|
||||
# Save the device
|
||||
device_information = {
|
||||
"device": {
|
||||
"name": potential_device_name,
|
||||
"additionalInfo": {
|
||||
"description": description,
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"credentialsType": "ACCESS_TOKEN",
|
||||
"credentialsId": str(uuid.uuid4()),
|
||||
}
|
||||
}
|
||||
saved_device = save_new_device(device_information)
|
||||
write_user_message(potential_device_name + " appareil créé avec succès.", "info")
|
||||
|
||||
#Save the device attributes
|
||||
attributes = {
|
||||
"sector_code": device[0],
|
||||
"serial_number": device[1],
|
||||
"data_type": device[2],
|
||||
"sector_wording": device[3],
|
||||
"inventory_number": device_inventory_number,
|
||||
}
|
||||
save_entity_attribute(json.loads(saved_device)["id"]["id"], attributes)
|
||||
write_user_message(potential_device_name + " attributs créés avec succès", "info")
|
||||
|
||||
except Exception as e:
|
||||
write_user_message(device_sector_code + " : " + str(e), "exception")
|
||||
@@ -0,0 +1,36 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app.utils.write_user_message import write_user_message
|
||||
|
||||
|
||||
def format_history(file_path):
|
||||
"""
|
||||
Create and format a Pandas DataFrame to be accepted by ThingsBoard.
|
||||
:param file_path: The path of the file to be read.
|
||||
:type file_path: String
|
||||
:return: Formatted Pandas DataFrame.
|
||||
:rtype: pandas.DataFrame
|
||||
"""
|
||||
|
||||
write_user_message("Création et formatage du DataFrame en cours...", "info")
|
||||
|
||||
data_frame = pd.read_csv(file_path, sep=";", header=0)
|
||||
|
||||
data_frame = data_frame.drop(columns=["nomSecteur"])
|
||||
|
||||
# Convert the datetime from String to timestamp
|
||||
data_frame["timestamp"] = pd.to_datetime(
|
||||
data_frame["date_temp"] + " " + data_frame["heure_temp"]
|
||||
)
|
||||
data_frame = data_frame.drop(columns=["date_temp", "heure_temp"])
|
||||
data_frame["timestamp"] = data_frame["timestamp"].astype('int64') // 1000
|
||||
|
||||
# Convert data from String to float
|
||||
data_frame["Data"] = data_frame["Data"].str.replace(',', '.', regex=False).astype(float)
|
||||
|
||||
|
||||
if data_frame.columns.size != 2:
|
||||
raise Exception("Après formatage, le DataFrame ne possède pas le bon nombre de colonne (2).")
|
||||
|
||||
return data_frame
|
||||
@@ -0,0 +1,97 @@
|
||||
import json
|
||||
|
||||
from app.utils.devices.get_user_device import get_user_device
|
||||
from app.utils.devices.save_entity_telemetry import save_entity_telemetry
|
||||
from app.database.queries.is_sector_code_existing import is_sector_code_existing
|
||||
from app.utils.write_user_message import write_user_message
|
||||
|
||||
|
||||
def save_history(file_name, data_frame, user_id):
|
||||
"""
|
||||
Saves telemetry dictionary to the ThingsBoard database.
|
||||
:param user_id: The id of the current ThingsBoard user
|
||||
:type user_id: String
|
||||
:param file_name: The name of the file that contains the raw data.
|
||||
:type file_name: String
|
||||
:param data_frame: Formatted Pandas DataFrame containing the data to be saved.
|
||||
:type data_frame: pandas.DataFrame
|
||||
:return: No return
|
||||
"""
|
||||
|
||||
sector_code = file_name.split("-")[0]
|
||||
is_sector_code_existing(sector_code)
|
||||
|
||||
device = is_device_existing(file_name, user_id)
|
||||
if device is None:
|
||||
raise Exception("L'appareil n'existe pas sur ThingsBoard.")
|
||||
|
||||
telemetry = dataframe_to_telemetry(data_frame, file_name)
|
||||
|
||||
# Save telemetry by chunk because data is too large
|
||||
chunk_size = 20000
|
||||
for i in range(0, len(telemetry), chunk_size):
|
||||
chunk = telemetry[i:i + chunk_size]
|
||||
|
||||
save_entity_telemetry(
|
||||
device['id']['entityType'],
|
||||
device['id']['id'],
|
||||
chunk
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def is_device_existing(file_name, user_id):
|
||||
"""
|
||||
Checks that the device corresponding to the sensor exists on ThingsBoard.
|
||||
:param file_name: The name of the file that is being processed.
|
||||
:type file_name: String
|
||||
:param user_id: The id of the current ThingsBoard user
|
||||
:type user_id: String
|
||||
:return: Data of the device
|
||||
:rtype: Dictionary
|
||||
"""
|
||||
write_user_message("Vérification en cours de l'existence de l'appareil sur ThingsBoard...", "info")
|
||||
|
||||
sector_code = file_name.split("-")[0]
|
||||
device_name = sector_code + "-Historique"
|
||||
|
||||
devices = get_user_device(user_id, device_name)
|
||||
jsoned_devices = json.loads(devices)
|
||||
|
||||
if len(jsoned_devices["data"]) == 0:
|
||||
raise Exception("L'appareil n'existe pas sur ThingsBoard.")
|
||||
|
||||
if jsoned_devices["hasNext"]:
|
||||
raise Exception("Plus d'un appareil correspond au même capteur sur ThingsBoard.")
|
||||
|
||||
return jsoned_devices["data"][0]
|
||||
|
||||
|
||||
|
||||
def dataframe_to_telemetry(df, file_name):
|
||||
"""
|
||||
Converts a pandas DataFrame into a telemetry dictionary.
|
||||
:param df: Pandas DataFrame containing the data to be converted.
|
||||
:type df: pandas.DataFrame
|
||||
:param file_name: The name of the file that is being processed.
|
||||
:type file_name: String
|
||||
:return: Dictionary of time series telemetry
|
||||
:rtype: Dictionary
|
||||
"""
|
||||
write_user_message("Conversion du DataFrame à un dictionnaire de télémétrie en cours...", "info")
|
||||
telemetry = []
|
||||
data_type = file_name.split("-")[1].split(".")[0]
|
||||
|
||||
for row in df.to_dict("records"):
|
||||
data = {
|
||||
"ts": row["timestamp"],
|
||||
"values": {
|
||||
data_type: row["Data"]
|
||||
}
|
||||
}
|
||||
|
||||
telemetry.append(data)
|
||||
|
||||
return telemetry
|
||||
@@ -0,0 +1,44 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app.utils.write_user_message import write_user_message
|
||||
|
||||
|
||||
def format_telemetry(file_path, file_name):
|
||||
"""
|
||||
Create and format a Pandas DataFrame to be accepted by ThingsBoard.
|
||||
:param file_path: The path of the file to be read.
|
||||
:type file_path: String
|
||||
:param file_name: The name of the file containing the raw data.
|
||||
:type file_name: String
|
||||
:return: Formatted Pandas DataFrame.
|
||||
:rtype: pandas.DataFrame
|
||||
"""
|
||||
|
||||
write_user_message("Création et formatage du DataFrame en cours...", "info")
|
||||
|
||||
data_frame = pd.read_csv(file_path, sep=";", header=1)
|
||||
data_type = file_name.split("-")[1]
|
||||
|
||||
#Drop useless column
|
||||
data_frame = data_frame.drop("#", axis=1, errors='ignore')
|
||||
|
||||
#Drop None columns and rows
|
||||
data_frame = data_frame.replace("Enregistré", np.nan)
|
||||
data_frame = data_frame.dropna(axis=1, how="all")
|
||||
data_frame = data_frame.dropna(axis=0, how="any")
|
||||
|
||||
#Convert the datetime from String to timestamp
|
||||
date_time = pd.to_datetime(data_frame[data_frame.columns[0]], dayfirst=True, errors='coerce')
|
||||
data_frame[data_frame.columns[0]] = date_time.astype('int64') // 1000
|
||||
|
||||
#Convert data from String to float
|
||||
data_frame[data_frame.columns[1]] = data_frame[data_frame.columns[1]].str.replace(',', '.', regex=False).astype(float)
|
||||
|
||||
if data_frame.columns.size != 2:
|
||||
raise Exception("Après formatage, le DataFrame ne possède pas le bon nombre de colonne (2).")
|
||||
|
||||
#Rename data column name to data_type value
|
||||
data_frame = data_frame.rename(columns={data_frame.columns[1]: data_type})
|
||||
|
||||
return data_frame
|
||||
@@ -0,0 +1,88 @@
|
||||
import json
|
||||
|
||||
from app.database.queries.get_device_inventory_number import get_device_inventory_number
|
||||
from app.utils.devices.get_user_device import get_user_device
|
||||
from app.utils.devices.save_entity_telemetry import save_entity_telemetry
|
||||
from app.database.queries.is_sector_code_existing import is_sector_code_existing
|
||||
from app.utils.write_user_message import write_user_message
|
||||
|
||||
|
||||
def save_telemetry(file_name, data_frame, user_id):
|
||||
"""
|
||||
Saves telemetry dictionary to the ThingsBoard database.
|
||||
:param user_id: The id of the current ThingsBoard user
|
||||
:type user_id: String
|
||||
:param file_name: The name of the file that contains the raw data.
|
||||
:type file_name: String
|
||||
:param data_frame: Formatted Pandas DataFrame containing the data to be saved.
|
||||
:type data_frame: pandas.DataFrame
|
||||
:return: No return
|
||||
"""
|
||||
|
||||
sector_code = file_name.split("-")[0]
|
||||
is_sector_code_existing(sector_code)
|
||||
|
||||
device = is_device_existing(file_name, user_id)
|
||||
if device is None:
|
||||
raise Exception("L'appareil n'existe pas sur ThingsBoard.")
|
||||
|
||||
telemetry = dataframe_to_telemetry(data_frame)
|
||||
|
||||
save_entity_telemetry(device['id']['entityType'], device['id']['id'], telemetry)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def is_device_existing(file_name, user_id):
|
||||
"""
|
||||
Checks that the device corresponding to the sensor exists on ThingsBoard.
|
||||
:param file_name: The name of the file that is being processed.
|
||||
:type file_name: String
|
||||
:param user_id: The id of the current ThingsBoard user
|
||||
:type user_id: String
|
||||
:return: Data of the device
|
||||
:rtype: Dictionary
|
||||
"""
|
||||
write_user_message("Vérification en cours de l'existence de l'appareil sur ThingsBoard...", "info")
|
||||
|
||||
device_serial_number = file_name.split("-")[4].split(".")[0]
|
||||
sector_code = file_name.split("-")[2]
|
||||
|
||||
device_inventory_number = get_device_inventory_number(device_serial_number)
|
||||
device_name = sector_code + "-" + device_inventory_number
|
||||
|
||||
devices = get_user_device(user_id, device_name)
|
||||
jsoned_devices = json.loads(devices)
|
||||
|
||||
if len(jsoned_devices["data"]) == 0:
|
||||
raise Exception("L'appareil n'existe pas sur ThingsBoard.")
|
||||
|
||||
if jsoned_devices["hasNext"]:
|
||||
raise Exception("Plus d'un appareil correspond au même capteur sur ThingsBoard.")
|
||||
|
||||
return jsoned_devices["data"][0]
|
||||
|
||||
|
||||
|
||||
def dataframe_to_telemetry(df):
|
||||
"""
|
||||
Converts a pandas DataFrame into a telemetry dictionary.
|
||||
:param df: Pandas DataFrame containing the data to be converted.
|
||||
:type df: pandas.DataFrame
|
||||
:return: Dictionary of time series telemetry
|
||||
:rtype: Dictionary
|
||||
"""
|
||||
write_user_message("Conversion du DataFrame à un dictionnaire de télémétrie en cours...", "info")
|
||||
telemetry = []
|
||||
for row in df.values:
|
||||
data = {
|
||||
"ts": row[0],
|
||||
"values": {
|
||||
df.columns[1]: row[1]
|
||||
}
|
||||
}
|
||||
|
||||
telemetry.append(data)
|
||||
|
||||
return telemetry
|
||||
@@ -0,0 +1,7 @@
|
||||
from app.utils.fetch_wrapper import fetch_wrapper
|
||||
|
||||
|
||||
def get_current_user():
|
||||
return fetch_wrapper(
|
||||
url = "auth/user",
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
from app.utils.fetch_wrapper import fetch_wrapper
|
||||
|
||||
|
||||
def login(data):
|
||||
return fetch_wrapper(
|
||||
url = "auth/login",
|
||||
method="post",
|
||||
data=data,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
from app.utils.fetch_wrapper import fetch_wrapper
|
||||
|
||||
|
||||
def logout():
|
||||
return fetch_wrapper(
|
||||
url = "auth/logout",
|
||||
method="post",
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
from app.utils.fetch_wrapper import fetch_wrapper
|
||||
|
||||
|
||||
def get_user_device(user_id, device_name):
|
||||
return fetch_wrapper(
|
||||
url = "customer/" + user_id + "/devices?pageSize=1&page=0&textSearch=" + device_name,
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
from app.utils.fetch_wrapper import fetch_wrapper
|
||||
|
||||
|
||||
def save_entity_attribute(device_id, data):
|
||||
return fetch_wrapper(
|
||||
url = "plugins/telemetry/" + device_id + "/SERVER_SCOPE",
|
||||
method = "post",
|
||||
data = data
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
from app.utils.fetch_wrapper import fetch_wrapper
|
||||
|
||||
|
||||
def save_entity_telemetry(entity_type, device_id, data):
|
||||
return fetch_wrapper(
|
||||
url = "plugins/telemetry/" + entity_type + "/" + device_id + "/timeseries/ANY",
|
||||
method = "post",
|
||||
data = data
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
from app.utils.fetch_wrapper import fetch_wrapper
|
||||
|
||||
|
||||
def save_new_device(device):
|
||||
return fetch_wrapper(
|
||||
url = "device-with-credentials?nameConflictPolicy=FAIL&uniquifySeparator=_&uniquifyStrategy=RANDOM",
|
||||
method = "post",
|
||||
data = device,
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
import requests as rq
|
||||
|
||||
from app.config import config
|
||||
|
||||
|
||||
def fetch_wrapper(
|
||||
url,
|
||||
method = "get",
|
||||
data = {}
|
||||
):
|
||||
res = getattr(rq, method)(
|
||||
url = config.thingsboard_url + url,
|
||||
headers = {
|
||||
"X-Authorization": "ApiKey " + config.thingsboard_user[6],
|
||||
"Accept": "application/json",
|
||||
},
|
||||
json = data,
|
||||
)
|
||||
if res.status_code != 200:
|
||||
raise Exception(res.text)
|
||||
|
||||
return res.text
|
||||
@@ -0,0 +1,22 @@
|
||||
from app.config import config
|
||||
import datetime
|
||||
|
||||
|
||||
class Color:
|
||||
RED = "\033[31m"
|
||||
YELLOW = "\033[33m"
|
||||
RESET = "\033[0m"
|
||||
|
||||
|
||||
def write_user_message(message, status):
|
||||
colors = {
|
||||
"warning": Color.YELLOW,
|
||||
"error": Color.RED,
|
||||
"exception": Color.RED,
|
||||
}
|
||||
color = colors.get(status, "")
|
||||
|
||||
now = datetime.datetime.now()
|
||||
|
||||
print(color + str(now) + ":" + status.upper() + ":" + message + Color.RESET)
|
||||
getattr(config.logger, status)(str(now) + ":" + message)
|
||||
@@ -0,0 +1,9 @@
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
from app import init_app
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_app()
|
||||
@@ -0,0 +1,11 @@
|
||||
certifi==2026.2.25
|
||||
charset-normalizer==3.4.7
|
||||
idna==3.11
|
||||
numpy==2.4.4
|
||||
pandas==3.0.2
|
||||
psycopg2-binary==2.9.12
|
||||
python-dateutil==2.9.0.post0
|
||||
python-dotenv==1.2.2
|
||||
requests==2.33.1
|
||||
six==1.17.0
|
||||
urllib3==2.6.3
|
||||
Reference in New Issue
Block a user